Skip to content

R&D new Microservice to authorize based on user ability#474

Open
dipesh-rumsan wants to merge 5 commits intodevfrom
feat/shared-auth
Open

R&D new Microservice to authorize based on user ability#474
dipesh-rumsan wants to merge 5 commits intodevfrom
feat/shared-auth

Conversation

@dipesh-rumsan
Copy link
Contributor

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new shared-auth microservice to handle user authorization based on CASL abilities. The microservice provides centralized authorization logic that can be consumed by other services via Redis message patterns, implementing role-based and permission-based access control with caching support.

Changes:

  • Added new shared-auth microservice with ability checking and user abilities retrieval functionality
  • Integrated SDK exports for shared-auth job events to enable cross-service communication
  • Updated build scripts and package.json to include the new microservice in the monorepo structure

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
package.json Added bcrypt dependencies (with version issues) and build/serve scripts for shared-auth microservice; added packageManager field for Corepack
libs/sdk/src/shared-auth/shared-auth.event.ts Defined job event constants for shared-auth service (CHECK_ABILITY, GET_USER_ABILITIES)
libs/sdk/src/shared-auth/index.ts Exported SharedAuthJobs for use in other services
libs/sdk/src/index.ts Added shared-auth exports to main SDK entry point
apps/shared-auth/webpack.config.js Standard NX webpack configuration for Node.js microservice
apps/shared-auth/tsconfig.spec.json TypeScript configuration for test files
apps/shared-auth/tsconfig.json Root TypeScript configuration with project references
apps/shared-auth/tsconfig.app.json TypeScript configuration for application source code
apps/shared-auth/src/types.ts Defined UserContext and CaslRule interfaces for authorization
apps/shared-auth/src/main.ts Microservice bootstrap with Redis transport configuration
apps/shared-auth/src/auth/auth.helpers.ts Helper functions for loading user roles/permissions and building JWTs
apps/shared-auth/src/app/app.module.ts Root module importing ConfigModule, PrismaModule, and AbilityModule
apps/shared-auth/src/ability/ability.service.ts Core authorization logic with caching, ability checking, and permission retrieval
apps/shared-auth/src/ability/ability.module.ts Module configuration for ability feature
apps/shared-auth/src/ability/ability.controller.ts Message pattern handlers for authorization requests
apps/shared-auth/project.json NX project configuration for build and serve targets
apps/shared-auth/modify-package.js Script to modify generated package.json with Prisma dependencies
apps/shared-auth/jest.config.ts Jest test configuration (with incorrect naming)
apps/shared-auth/.eslintrc.json ESLint configuration extending base rules
apps/rahat/src/projects/project.service.ts Temporary change to always send user context in RPC calls

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +55 to +66
console.log('user obj is: ', permissions);

const ability = this.buildAbility(permissions);

const allowed = ability.can(action, subject);
console.log(
`user allowed ${allowed} for action ${action} subject ${subject}`
);

const rule = ability.relevantRuleFor(action, subject);
console.log(
`user rule ${allowed} for action ${action} subject ${subject}`
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statements should be removed or replaced with proper logger calls before merging to production. This code contains three console.log statements that will clutter production logs. Replace these with this.logger.log() or this.logger.debug() calls, or remove them entirely.

Suggested change
console.log('user obj is: ', permissions);
const ability = this.buildAbility(permissions);
const allowed = ability.can(action, subject);
console.log(
`user allowed ${allowed} for action ${action} subject ${subject}`
);
const rule = ability.relevantRuleFor(action, subject);
console.log(
`user rule ${allowed} for action ${action} subject ${subject}`
this.logger.debug(`User permissions: ${JSON.stringify(permissions)}`);
const ability = this.buildAbility(permissions);
const allowed = ability.can(action, subject);
this.logger.debug(
`User allowed: ${allowed} for action "${action}" on subject "${subject}"`
);
const rule = ability.relevantRuleFor(action, subject);
this.logger.debug(
`User rule for action "${action}" on subject "${subject}": ${JSON.stringify(rule)}`

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +42
const { userId, action, subject } = payload;

const cacheKey = `ability:${userId}:${action}:${subject}`;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache key does not include the conditions parameter, which means ability checks with different conditions for the same userId/action/subject will incorrectly return the same cached result. This could lead to incorrect authorization decisions when conditional permissions are involved. The cache key should incorporate a hash or stringified version of the conditions when they are present.

Suggested change
const { userId, action, subject } = payload;
const cacheKey = `ability:${userId}:${action}:${subject}`;
const { userId, action, subject, conditions } = payload;
const conditionsKey = conditions ? JSON.stringify(conditions) : 'no-conditions';
const cacheKey = `ability:${userId}:${action}:${subject}:${conditionsKey}`;

Copilot uses AI. Check for mistakes.
.send(cmd, {
...payload,
...(requiresUser && { user }),
...(true && { user }), // temp solution to send user context for actions that require authentication, will refactor later
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temporary solution comment indicates this code change is incomplete. The condition ...(true && { user }) always evaluates to true, which means the user context is always sent regardless of whether the action requires authentication. This bypasses the requiresUser check on line 165 that was presumably meant to control this behavior. This should either be changed to ...(requiresUser && { user }) to use the actual check, or the requiresUser logic should be removed entirely if it's no longer needed.

Suggested change
...(true && { user }), // temp solution to send user context for actions that require authentication, will refactor later
...(requiresUser && { user }), // send user context only for actions that require authentication

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +29
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import {
SharedAuthJobs
} from '@rahataid/sdk';
import { AbilityService } from './ability.service';

@Controller()
export class AbilityController {
constructor(private readonly abilityService: AbilityService) { }

@MessagePattern({ cmd: SharedAuthJobs.CHECK_ABILITY })
checkAbility(
@Payload()
payload: {
userId: number;
action: string;
subject: string;
conditions?: any;
},
) {
return this.abilityService.checkAbility(payload);
}

@MessagePattern({ cmd: SharedAuthJobs.GET_USER_ABILITIES })
getUserAbilities(@Payload() payload: { userId: number }) {
return this.abilityService.getUserAbilities(payload);
}
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AbilityController lacks test coverage. Based on the codebase convention (e.g., apps/beneficiary/src/beneficiary/beneficiary.controller.spec.ts), controllers should have accompanying .spec.ts files with unit tests. The controller handles message patterns for authorization checks and should be tested to ensure proper request/response handling and error scenarios.

Copilot uses AI. Check for mistakes.
"amqp-connection-manager": "^4.1.14",
"amqplib": "^0.10.5",
"axios": "^1.6.7",
"bcrypt": "^6.0.0",
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bcrypt package is added to dependencies but is not used anywhere in the shared-auth microservice code. This creates an unused dependency that increases the application bundle size and maintenance burden. Consider removing this dependency unless it will be used in upcoming commits.

Suggested change
"bcrypt": "^6.0.0",

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +29
this.redis = new Redis({
host: this.configService.get<string>('REDIS_HOST', 'localhost'),
port: this.configService.get<number>('REDIS_PORT', 6379),
password: this.configService.get<string>('REDIS_PASSWORD'),
});
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Redis client is instantiated in the constructor without proper error handling. If the Redis connection fails (e.g., due to incorrect credentials or network issues), the microservice will throw an unhandled error and potentially crash. Consider adding error event listeners (redis.on('error', ...)) and implementing a connection retry strategy, or using a Redis connection pool/manager pattern for better resilience.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +116
@Injectable()
export class AbilityService {
private readonly logger = new Logger(AbilityService.name);
private readonly redis: Redis;
private readonly cacheTtl: number = 300;

constructor(
private readonly prisma: PrismaService,
private readonly configService: ConfigService
) {
this.redis = new Redis({
host: this.configService.get<string>('REDIS_HOST', 'localhost'),
port: this.configService.get<number>('REDIS_PORT', 6379),
password: this.configService.get<string>('REDIS_PASSWORD'),
});
}

/** Check if a user can perform an action on a subject. */
async checkAbility(payload: {
userId: number;
action: string;
subject: string;
conditions?: any;
}): Promise<{ allowed: boolean; reason?: string }> {
try {
const { userId, action, subject } = payload;

const cacheKey = `ability:${userId}:${action}:${subject}`;
const cached = await this.redis.get(cacheKey);

if (cached) {
this.logger.log(`Getting data from cache using key ${cacheKey}`);
return JSON.parse(cached);
}

// Build ability from user's roles and permissions
const { permissions } = await loadUserRolesAndPermissions(
this.prisma,
userId
);
console.log('user obj is: ', permissions);

const ability = this.buildAbility(permissions);

const allowed = ability.can(action, subject);
console.log(
`user allowed ${allowed} for action ${action} subject ${subject}`
);

const rule = ability.relevantRuleFor(action, subject);
console.log(
`user rule ${allowed} for action ${action} subject ${subject}`
);

const reason = rule?.reason;
const result = { allowed, ...(reason ? { reason } : {}) };

// Cache the result
await this.redis.set(
cacheKey,
JSON.stringify(result),
'EX',
this.cacheTtl
);

return result;
} catch (error) {
this.logger.error('checkAbility failed', error);
return { allowed: false, reason: 'Ability check failed' };
}
}

/** Return all CASL rules for a user. */
async getUserAbilities(payload: {
userId: number;
}): Promise<{ rules: CaslRule[] }> {
try {
const { permissions } = await loadUserRolesAndPermissions(
this.prisma,
payload.userId
);
return { rules: permissions };
} catch (error) {
this.logger.error('getUserAbilities failed', error);
return { rules: [] };
}
}

private buildAbility(permissions: CaslRule[]): AppAbility {
const builder = new AbilityBuilder<AppAbility>(createMongoAbility);

for (const perm of permissions) {
if (perm.inverted) {
builder.cannot(perm.action, perm.subject);
} else {
builder.can(perm.action, perm.subject);
}
}

return builder.build();
}
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Redis client connection is never closed when the service is destroyed. This can lead to connection leaks, especially during testing or application shutdowns. Implement the OnModuleDestroy interface and add a cleanup method to properly disconnect the Redis client: async onModuleDestroy() { await this.redis.quit(); }

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +116
@Injectable()
export class AbilityService {
private readonly logger = new Logger(AbilityService.name);
private readonly redis: Redis;
private readonly cacheTtl: number = 300;

constructor(
private readonly prisma: PrismaService,
private readonly configService: ConfigService
) {
this.redis = new Redis({
host: this.configService.get<string>('REDIS_HOST', 'localhost'),
port: this.configService.get<number>('REDIS_PORT', 6379),
password: this.configService.get<string>('REDIS_PASSWORD'),
});
}

/** Check if a user can perform an action on a subject. */
async checkAbility(payload: {
userId: number;
action: string;
subject: string;
conditions?: any;
}): Promise<{ allowed: boolean; reason?: string }> {
try {
const { userId, action, subject } = payload;

const cacheKey = `ability:${userId}:${action}:${subject}`;
const cached = await this.redis.get(cacheKey);

if (cached) {
this.logger.log(`Getting data from cache using key ${cacheKey}`);
return JSON.parse(cached);
}

// Build ability from user's roles and permissions
const { permissions } = await loadUserRolesAndPermissions(
this.prisma,
userId
);
console.log('user obj is: ', permissions);

const ability = this.buildAbility(permissions);

const allowed = ability.can(action, subject);
console.log(
`user allowed ${allowed} for action ${action} subject ${subject}`
);

const rule = ability.relevantRuleFor(action, subject);
console.log(
`user rule ${allowed} for action ${action} subject ${subject}`
);

const reason = rule?.reason;
const result = { allowed, ...(reason ? { reason } : {}) };

// Cache the result
await this.redis.set(
cacheKey,
JSON.stringify(result),
'EX',
this.cacheTtl
);

return result;
} catch (error) {
this.logger.error('checkAbility failed', error);
return { allowed: false, reason: 'Ability check failed' };
}
}

/** Return all CASL rules for a user. */
async getUserAbilities(payload: {
userId: number;
}): Promise<{ rules: CaslRule[] }> {
try {
const { permissions } = await loadUserRolesAndPermissions(
this.prisma,
payload.userId
);
return { rules: permissions };
} catch (error) {
this.logger.error('getUserAbilities failed', error);
return { rules: [] };
}
}

private buildAbility(permissions: CaslRule[]): AppAbility {
const builder = new AbilityBuilder<AppAbility>(createMongoAbility);

for (const perm of permissions) {
if (perm.inverted) {
builder.cannot(perm.action, perm.subject);
} else {
builder.can(perm.action, perm.subject);
}
}

return builder.build();
}
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AbilityService lacks test coverage. Based on the codebase convention (e.g., apps/beneficiary/src/beneficiary/beneficiary.service.spec.ts), services should have accompanying .spec.ts files with unit tests. The service contains critical authorization logic that should be thoroughly tested, including scenarios for permission checking, caching behavior, role expiry, and error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +103 to +114
private buildAbility(permissions: CaslRule[]): AppAbility {
const builder = new AbilityBuilder<AppAbility>(createMongoAbility);

for (const perm of permissions) {
if (perm.inverted) {
builder.cannot(perm.action, perm.subject);
} else {
builder.can(perm.action, perm.subject);
}
}

return builder.build();
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buildAbility method ignores the conditions field from CaslRule permissions. The CaslRule type includes an optional conditions field for fine-grained access control, but the buildAbility method only uses action and subject when building abilities. This means any conditional permissions defined in the database will be ignored, potentially allowing broader access than intended. You should pass conditions to builder.can() and builder.cannot() methods when they exist.

Copilot uses AI. Check for mistakes.
import { AppModule } from './app/app.module';

async function bootstrap() {
const PORT: number = +process.env.PORT_AUTH || 8080;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PORT_AUTH environment variable is used but not documented in .env.example. This will cause confusion for developers setting up the application, as they won't know this environment variable needs to be configured. Add PORT_AUTH to .env.example with an appropriate default value (e.g., PORT_AUTH=5502 to follow the pattern of PORT=5500 and PORT_BEN=5501).

Suggested change
const PORT: number = +process.env.PORT_AUTH || 8080;
const PORT: number = +process.env.PORT || 8080;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants