@ahksolution/permissions-sdk
v1.3.1
Published
gRPC client SDK for AHK Solution Permissions Microservice - provides NestJS guard, decorators, and client for inter-service permission checks
Maintainers
Readme
@ahksolution/permissions-sdk
gRPC client SDK for the AHK Solution Permissions Microservice. Provides NestJS integration for:
- JWT Authentication - Validate tokens via gRPC (no JWT secret needed in consuming services)
- Permission Checks - RBAC and ABAC support via gRPC
Installation
# From private npm registry
npm install @ahksolution/permissions-sdk
# Or using pnpm
pnpm add @ahksolution/permissions-sdkPeer Dependencies (must be installed in your project):
pnpm add @nestjs/microservices @grpc/grpc-js @grpc/proto-loaderQuick Start
1. Register the Module
// app.module.ts
import { Module } from '@nestjs/common';
import { PermissionsClientModule } from '@ahksolution/permissions-sdk';
@Module({
imports: [
// Static configuration
PermissionsClientModule.register({
url: 'localhost:50051',
}),
],
})
export class AppModule {}With async configuration (recommended for production):
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PermissionsClientModule } from '@ahksolution/permissions-sdk';
@Module({
imports: [
ConfigModule.forRoot(),
PermissionsClientModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
url: config.get<string>('PERMISSIONS_SERVICE_URL', 'localhost:50051'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}2. Apply Guards Globally (Recommended)
For full authentication and authorization, apply both guards globally:
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import {
PermissionsClientModule,
JwtAuthGuard,
PermissionsGuard,
} from '@ahksolution/permissions-sdk';
@Module({
imports: [PermissionsClientModule.register({ url: 'localhost:50051' })],
providers: [
// Order matters: authentication first, then authorization
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: PermissionsGuard },
],
})
export class AppModule {}3. Use Decorators in Controllers
import { Controller, Get, Post, Body } from '@nestjs/common';
import { Public, CurrentUser, RequirePermissions, JwtUserData } from '@ahksolution/permissions-sdk';
@Controller('orders')
export class OrdersController {
// Public route - bypasses JWT authentication
@Public()
@Get('health')
health() {
return { status: 'ok' };
}
// Authenticated route - no specific permission required
@Get('me')
getProfile(@CurrentUser() user: JwtUserData) {
return {
id: user.id,
email: user.email,
roles: user.roles,
};
}
// Authenticated + requires specific permission
@Post()
@RequirePermissions('orders:create')
createOrder(@CurrentUser() user: JwtUserData, @Body() dto: CreateOrderDto) {
return this.orderService.create(user.id, dto);
}
// Get specific user property
@Get('my-orders')
@RequirePermissions('orders:read')
getMyOrders(@CurrentUser('id') userId: string) {
return this.orderService.findByUser(userId);
}
}JWT Authentication
The SDK provides JWT authentication that validates tokens via gRPC call to the permissions service. No JWT secret is required in consuming services - all validation happens centrally.
How It Works
JwtAuthGuardextracts the token fromAuthorization: Bearer <token>header- Calls
ValidateTokengRPC method on the permissions service - Permissions service verifies the token and returns user data
- User data (with roles and permissions) is attached to
request.user
JwtUserData Type
interface JwtUserData {
id: string;
email: string | null;
phone: string | null;
userType: string;
status: string;
isProfileComplete: boolean;
roles: RoleInfo[]; // User's roles
permissions: string[]; // User's permission codes
hasAllAccess: boolean; // True if user has wildcard access
}
interface RoleInfo {
id: string;
code: string;
name: string;
isSystem: boolean;
}Decorators
| Decorator | Description |
| -------------------- | ------------------------------------------------ |
| @Public() | Mark route as public (bypass JWT authentication) |
| @CurrentUser() | Get full user object from request |
| @CurrentUser('id') | Get specific property from user |
Permission Checking
Using Decorators (Recommended)
import {
RequirePermissions,
RequireAnyPermission,
RequireAllPermissions,
} from '@ahksolution/permissions-sdk';
@Controller('orders')
export class OrdersController {
// Require a single permission
@Post()
@RequirePermissions('orders:create')
create() {}
// Require ALL permissions (AND logic) - default behavior
@Post(':id/approve')
@RequirePermissions(['orders:read', 'orders:approve'])
approve() {}
// Require ANY of the permissions (OR logic)
@Delete(':id')
@RequirePermissions(['orders:delete', 'admin:full'], { mode: 'any' })
delete() {}
// Using alias decorators for clarity
@Post(':id/export')
@RequireAllPermissions(['orders:read', 'orders:export'])
export() {}
@Post(':id/cancel')
@RequireAnyPermission(['orders:cancel', 'orders:manage'])
cancel() {}
}Using the Client Service
Inject PermissionsGrpcClient to check permissions programmatically:
import { Injectable } from '@nestjs/common';
import { PermissionsGrpcClient } from '@ahksolution/permissions-sdk';
@Injectable()
export class OrderService {
constructor(private readonly permissions: PermissionsGrpcClient) {}
async createOrder(userId: string, orderData: CreateOrderDto) {
// Simple boolean check
const canCreate = await this.permissions.hasPermission(userId, 'orders:create');
if (!canCreate) {
throw new ForbiddenException('You do not have permission to create orders');
}
// Continue with order creation...
}
async deleteOrder(userId: string, orderId: string) {
// Check multiple permissions (user needs ANY of these)
const canDelete = await this.permissions.hasAnyPermission(userId, [
'orders:delete',
'orders:manage',
'admin:full',
]);
if (!canDelete) {
throw new ForbiddenException('You do not have permission to delete orders');
}
// Continue with deletion...
}
}API Reference
Guards
| Guard | Description |
| ------------------ | ------------------------------------------------------------ |
| JwtAuthGuard | Validates JWT tokens via gRPC. Attaches user to request. |
| PermissionsGuard | Checks permissions based on @RequirePermissions decorator. |
PermissionsGrpcClient
| Method | Description |
| --------------------------------------------------------- | ----------------------------------------------------------- |
| validateToken(token) | Validates JWT and returns ValidateTokenResult |
| hasPermission(userId, permissionCode) | Returns boolean - does user have this permission? |
| hasAllPermissions(userId, permissionCodes) | Returns boolean - does user have ALL permissions? |
| hasAnyPermission(userId, permissionCodes) | Returns boolean - does user have ANY permission? |
| checkPermission(userId, permissionCode, options?) | Returns full EvaluationResult with details |
| checkBulkPermissions(userId, permissionCodes, options?) | Returns results for multiple permissions |
| getEffectivePermissions(userId) | Returns all permissions and roles for a user |
| getUserInfo(userId) | Returns complete user profile with roles & permissions |
| getUserRoles(userId) | Returns user's roles only |
| getUserPermissions(userId) | Returns user's permission codes only |
| getBulkUserInfo(userIds, options?) | Returns user data for multiple users with optional flags |
| createUser(options) | Creates a new user with validations and optional magic link |
Decorators
| Decorator | Description |
| -------------------------------------------- | -------------------------------------------- |
| @Public() | Mark route as public (bypass JWT auth) |
| @CurrentUser() | Get authenticated user from request |
| @CurrentUser('property') | Get specific property from user |
| @RequirePermissions(permissions, options?) | Require permission(s) with configurable mode |
| @RequireAllPermissions(permissions) | Shorthand for mode: 'all' |
| @RequireAnyPermission(permissions) | Shorthand for mode: 'any' |
Options
interface RequirePermissionsOptions {
// 'all' = user must have ALL permissions (default)
// 'any' = user must have at least ONE permission
mode?: 'all' | 'any';
// Custom error message when denied
errorMessage?: string;
// Include request params as resource context for ABAC
includeResourceContext?: boolean;
}Advanced Usage
Full Evaluation Result
Get detailed information about permission decisions:
const result = await this.permissions.checkPermission(userId, 'orders:create');
console.log(result);
// {
// allowed: true,
// source: 'rbac', // 'rbac' | 'abac' | 'break-glass' | 'denied'
// matchedRoles: ['ADMIN'],
// matchedPolicies: [],
// reason: 'Permission granted via role(s): ADMIN',
// evaluationTimeMs: 3
// }ABAC Context
Pass resource and request context for attribute-based access control:
const result = await this.permissions.checkPermission(userId, 'documents:read', {
resource: {
id: 'doc-123',
type: 'document',
ownerId: 'user-456',
department: 'engineering',
},
request: {
ip: '192.168.1.100',
method: 'GET',
path: '/api/documents/doc-123',
},
});Get All User Permissions
const effective = await this.permissions.getEffectivePermissions(userId);
console.log(effective);
// {
// permissions: ['users:read', 'users:create', 'orders:read'],
// roles: [
// { id: '...', code: 'USER', name: 'User', isSystem: false },
// { id: '...', code: 'ORDER_VIEWER', name: 'Order Viewer', isSystem: false }
// ],
// version: 1,
// computedAt: Date
// }User Data Methods (v1.2.0+)
Fetch user data directly by userId without token validation. Useful for service-to-service calls.
import { Injectable, NotFoundException } from '@nestjs/common';
import { PermissionsGrpcClient } from '@ahksolution/permissions-sdk';
@Injectable()
export class UserProfileService {
constructor(private readonly permissions: PermissionsGrpcClient) {}
// Get complete user profile with roles and permissions
async getUserProfile(userId: string) {
const result = await this.permissions.getUserInfo(userId);
if (!result.found) {
throw new NotFoundException(result.errorMessage); // 'USER_NOT_FOUND' | 'USER_INACTIVE'
}
return result.user;
// {
// id: '...',
// email: '[email protected]',
// phone: '+1234567890',
// userType: 'CUSTOMER',
// status: 'ACTIVE',
// isProfileComplete: true,
// roles: [{ id, code, name, isSystem }],
// permissions: ['orders:read', 'orders:create'],
// hasAllAccess: false
// }
}
// Get only user's roles
async getUserRoleNames(userId: string) {
const result = await this.permissions.getUserRoles(userId);
if (!result.found) {
throw new NotFoundException(result.errorMessage);
}
return result.roles.map((r) => r.name); // ['Admin', 'Manager']
}
// Get only user's permissions
async canAccessFeature(userId: string, featurePermission: string) {
const result = await this.permissions.getUserPermissions(userId);
if (!result.found) return false;
// Check if user has all access or the specific permission
return result.hasAllAccess || result.permissions.includes(featurePermission);
}
}Bulk User Data (v1.4.0+)
Fetch data for multiple users in a single gRPC call with optional flags to control what data is returned.
import { Injectable, NotFoundException } from '@nestjs/common';
import { PermissionsGrpcClient } from '@ahksolution/permissions-sdk';
@Injectable()
export class TeamService {
constructor(private readonly permissions: PermissionsGrpcClient) {}
// Get basic info for multiple users (no roles/permissions)
async getTeamMembers(userIds: string[]) {
const result = await this.permissions.getBulkUserInfo(userIds);
return Object.entries(result.users)
.filter(([_, item]) => item.found)
.map(([userId, item]) => ({
id: item.user!.id,
email: item.user!.email,
name: item.user!.profile?.fullName,
}));
}
// Get users with their roles included
async getTeamWithRoles(userIds: string[]) {
const result = await this.permissions.getBulkUserInfo(userIds, {
includeRoles: true,
});
return Object.entries(result.users)
.filter(([_, item]) => item.found)
.map(([userId, item]) => ({
id: item.user!.id,
email: item.user!.email,
roles: item.user!.roles.map((r) => r.name),
}));
}
// Get users with both roles and permissions
async getTeamWithFullAccess(userIds: string[]) {
const result = await this.permissions.getBulkUserInfo(userIds, {
includeRoles: true,
includePermissions: true,
});
return Object.entries(result.users).map(([userId, item]) => {
if (!item.found) {
return { userId, error: item.errorMessage };
}
return {
userId,
email: item.user!.email,
roles: item.user!.roles,
permissions: item.user!.permissions,
hasAllAccess: item.user!.hasAllAccess,
};
});
}
// Handle mixed results (some users found, some not)
async validateUserIds(userIds: string[]) {
const result = await this.permissions.getBulkUserInfo(userIds);
const found: string[] = [];
const notFound: string[] = [];
const inactive: string[] = [];
for (const [userId, item] of Object.entries(result.users)) {
if (item.found) {
found.push(userId);
} else if (item.errorMessage === 'USER_INACTIVE') {
inactive.push(userId);
} else {
notFound.push(userId);
}
}
return { found, notFound, inactive };
}
}Options:
| Option | Type | Default | Description |
| -------------------- | --------- | ------- | ------------------------------------ |
| includeRoles | boolean | false | Include user roles in response |
| includePermissions | boolean | false | Include user permissions in response |
Return Types:
interface GetBulkUserInfoResult {
users: Record<string, BulkUserInfoItem>;
}
interface BulkUserInfoItem {
found: boolean;
errorMessage?: string; // 'USER_NOT_FOUND' | 'USER_INACTIVE' | 'INTERNAL_ERROR'
user?: BulkUserData; // Only populated if found=true
}
interface BulkUserData {
id: string;
email: string | null;
phone: string | null;
userType: string;
status: string;
isProfileComplete: boolean;
roles: RoleInfo[]; // Empty array if includeRoles=false
permissions: string[]; // Empty array if includePermissions=false
hasAllAccess: boolean;
profile?: UserProfileData;
}
interface GetUserInfoResult {
found: boolean;
errorMessage?: string; // 'USER_NOT_FOUND' | 'USER_INACTIVE' | 'INTERNAL_ERROR'
user?: JwtUserData; // Only populated if found=true
}
interface GetUserRolesResult {
found: boolean;
errorMessage?: string;
roles: RoleInfo[];
}
interface GetUserPermissionsResult {
found: boolean;
errorMessage?: string;
permissions: string[];
hasAllAccess: boolean;
}Create User (v1.5.0+)
Create new users via gRPC with full validation support including email uniqueness, magic link sending, role assignment, and organization assignment.
import { Injectable, BadRequestException, ConflictException } from '@nestjs/common';
import { PermissionsGrpcClient, UserType } from '@ahksolution/permissions-sdk';
@Injectable()
export class UserManagementService {
constructor(private readonly permissions: PermissionsGrpcClient) {}
// Create an admin user with magic link
async createAdminUser(email: string, firstName: string, lastName: string) {
const result = await this.permissions.createUser({
firstName,
lastName,
email,
userType: UserType.ADMIN,
sendMagicLink: true, // Default: true
});
if (!result.success) {
// Handle specific error codes
if (result.errorCode === 'EMAIL_EXISTS') {
throw new ConflictException('A user with this email already exists');
}
throw new BadRequestException(result.errorMessage);
}
return {
userId: result.user!.id,
email: result.user!.email,
magicLinkSent: result.magicLinkSent,
};
}
// Create a customer user with roles and organization
async createCustomerWithRoles(data: {
firstName: string;
lastName: string;
email?: string;
phone?: string;
departmentId?: string;
designationId?: string;
roleIds?: string[];
}) {
const result = await this.permissions.createUser({
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
phone: data.phone,
userType: UserType.CUSTOMER,
departmentId: data.departmentId,
designationId: data.designationId,
roleIds: data.roleIds,
sendMagicLink: false, // Don't send magic link for customers
});
if (!result.success) {
this.handleCreateUserError(result.errorCode, result.errorMessage);
}
return result.user;
}
private handleCreateUserError(errorCode?: string, errorMessage?: string): never {
switch (errorCode) {
case 'EMAIL_EXISTS':
throw new ConflictException('Email already registered');
case 'PHONE_EXISTS':
throw new ConflictException('Phone number already registered');
case 'INVALID_ROLE':
throw new BadRequestException('One or more role IDs are invalid');
case 'INVALID_ORG':
throw new BadRequestException('Invalid department or designation ID');
case 'VALIDATION_ERROR':
throw new BadRequestException(errorMessage ?? 'Validation failed');
default:
throw new BadRequestException(errorMessage ?? 'Failed to create user');
}
}
}Options:
interface CreateUserOptions {
firstName: string; // Required
lastName: string; // Required
email?: string; // Required if phone not provided
phone?: string; // Required if email not provided
userType: UserType; // Required: UserType.ADMIN or UserType.CUSTOMER
departmentId?: string; // Optional: Department for org assignment
designationId?: string; // Optional: Designation for org assignment
roleIds?: string[]; // Optional: Role IDs to assign to user
sendMagicLink?: boolean; // Optional: Send magic link email (default: true)
}
enum UserType {
ADMIN = 'ADMIN',
CUSTOMER = 'CUSTOMER',
}Return Types:
interface CreateUserResult {
success: boolean;
errorCode?: string; // 'EMAIL_EXISTS' | 'PHONE_EXISTS' | 'INVALID_ROLE' | 'INVALID_ORG' | 'VALIDATION_ERROR' | 'INTERNAL_ERROR'
errorMessage?: string;
user?: CreatedUserData; // Only populated if success=true
magicLinkSent: boolean;
}
interface CreatedUserData {
id: string;
email: string | null;
phone: string | null;
userType: string;
status: string;
profile?: UserProfileData;
}Error Codes:
| Error Code | Description |
| ------------------ | -------------------------------------- |
| EMAIL_EXISTS | A user with this email already exists |
| PHONE_EXISTS | A user with this phone number exists |
| INVALID_ROLE | One or more role IDs are invalid |
| INVALID_ORG | Invalid department or designation ID |
| VALIDATION_ERROR | General validation error (see message) |
| INTERNAL_ERROR | Server-side error during user creation |
Environment Variables
| Variable | Description | Default |
| ------------------------- | ------------------- | ----------------- |
| PERMISSIONS_SERVICE_URL | gRPC server address | localhost:50051 |
Error Handling
The guard throws ForbiddenException when permission is denied:
// Default error
throw new ForbiddenException('Access denied. Required permission(s): orders:create');
// Custom error message
@RequirePermissions('orders:create', {
errorMessage: 'You need order creation privileges'
})Complete Example
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
PermissionsClientModule,
JwtAuthGuard,
PermissionsGuard,
} from '@ahksolution/permissions-sdk';
@Module({
imports: [
ConfigModule.forRoot(),
PermissionsClientModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
url: config.get('PERMISSIONS_SERVICE_URL', 'localhost:50051'),
}),
inject: [ConfigService],
}),
],
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: PermissionsGuard },
],
})
export class AppModule {}
// users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import {
Public,
CurrentUser,
RequirePermissions,
RequireAnyPermission,
JwtUserData,
} from '@ahksolution/permissions-sdk';
@Controller('users')
export class UsersController {
// Public endpoint - no auth required
@Public()
@Get('health')
health() {
return { status: 'ok' };
}
// Auth required, no specific permission
@Get('profile')
getProfile(@CurrentUser() user: JwtUserData) {
return user;
}
// Auth + specific permission required
@Get()
@RequirePermissions('users:list')
findAll() {
return this.userService.findAll();
}
@Post()
@RequirePermissions('users:create')
create(@Body() dto: CreateUserDto, @CurrentUser('id') createdBy: string) {
return this.userService.create(dto, createdBy);
}
@Get(':id')
@RequireAnyPermission(['users:read', 'users:manage'])
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
}Important Notes
No JWT Secret Required: The SDK validates tokens via gRPC call to the permissions service. You don't need to configure JWT secrets in consuming services.
Guard Order Matters: When using both guards globally,
JwtAuthGuardmust run beforePermissionsGuard(authentication before authorization).Permissions Service Required: This SDK is a client for the AHK Solution Permissions Microservice. It will not function without a running instance exposing a gRPC endpoint.
Same-Pod Deployment: For optimal performance, deploy consuming services in the same pod/network as the permissions service to minimize gRPC latency.
License
MIT
