- Updated privilege key structure to use a 3- or 4-part dotted hierarchy (e.g., `GROUP.PARENT.MODULE`). - Modified the `RequirePrivilege` decorator to accept multiple keys, allowing for OR logic in privilege checks. - Enhanced `PrivilegesGuard` to validate against multiple privilege keys, improving access control logic. - Created migration scripts to update existing privilege keys in the database to the new format. - Updated related services, controllers, and tests to accommodate the new privilege key structure and validation logic.
93 lines
2.5 KiB
TypeScript
93 lines
2.5 KiB
TypeScript
import {
|
|
CanActivate,
|
|
ExecutionContext,
|
|
ForbiddenException,
|
|
Injectable,
|
|
UnauthorizedException,
|
|
} from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import type { AuthUser } from '../../../common/auth/auth-user';
|
|
import type { PrivilegeAction } from '../../privileges/privilege-action';
|
|
import { PrivilegesService } from '../../privileges/privileges.service';
|
|
import {
|
|
fieldPrivilegeKeys,
|
|
isFieldPurpose,
|
|
type FieldPurpose,
|
|
type FieldResource,
|
|
} from './field-purpose';
|
|
import {
|
|
REQUIRE_FIELD_PRIVILEGE_KEY,
|
|
type RequireFieldPrivilegeMeta,
|
|
} from './field-privilege.decorator';
|
|
|
|
@Injectable()
|
|
export class FieldPrivilegeGuard implements CanActivate {
|
|
constructor(
|
|
private readonly reflector: Reflector,
|
|
private readonly privilegesService: PrivilegesService,
|
|
) {}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
const required = this.reflector.getAllAndOverride<
|
|
RequireFieldPrivilegeMeta | undefined
|
|
>(REQUIRE_FIELD_PRIVILEGE_KEY, [context.getHandler(), context.getClass()]);
|
|
|
|
if (!required) {
|
|
return true;
|
|
}
|
|
|
|
const request = context.switchToHttp().getRequest<{
|
|
user?: AuthUser;
|
|
body?: { purpose?: string };
|
|
query?: { purpose?: string };
|
|
}>();
|
|
const user = request.user;
|
|
if (!user) {
|
|
throw new UnauthorizedException();
|
|
}
|
|
if (user.isSuperadmin) {
|
|
return true;
|
|
}
|
|
|
|
const purposeRaw = request.body?.purpose ?? request.query?.purpose;
|
|
const allowed = await this.allowedPurposes(
|
|
user.id,
|
|
required.resource,
|
|
required.action,
|
|
);
|
|
if (purposeRaw !== undefined && purposeRaw !== '') {
|
|
if (!isFieldPurpose(purposeRaw)) {
|
|
throw new ForbiddenException('Insufficient privilege');
|
|
}
|
|
if (!allowed.includes(purposeRaw)) {
|
|
throw new ForbiddenException('Insufficient privilege');
|
|
}
|
|
return true;
|
|
}
|
|
if (allowed.length === 0) {
|
|
throw new ForbiddenException('Insufficient privilege');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async allowedPurposes(
|
|
userId: string,
|
|
resource: FieldResource,
|
|
action: PrivilegeAction,
|
|
): Promise<FieldPurpose[]> {
|
|
const purposes: FieldPurpose[] = ['sales', 'logistics'];
|
|
const matches: FieldPurpose[] = [];
|
|
for (const purpose of purposes) {
|
|
const ok = await this.privilegesService.checkAnyPermission(
|
|
userId,
|
|
fieldPrivilegeKeys(resource, purpose),
|
|
action,
|
|
);
|
|
if (ok) {
|
|
matches.push(purpose);
|
|
}
|
|
}
|
|
return matches;
|
|
}
|
|
}
|