diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index e8725ea..e88cc0f 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -37,6 +37,7 @@ import { RequireAllPermissions, } from 'src/decorators/authentication.decorator'; import { AuthClientService } from './auth.service'; +import { UserDTO } from './dtos/login'; import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; import { GrpcToHttpExceptionFilter } from '../../error/grpc-to-http-exception.filter'; import { RequestUser, User } from 'src/decorators/user.decorator'; @@ -486,7 +487,7 @@ export class AuthController { this.logger.info('Authenticating via X-Api-key header'); const { api_key } = await this.apiKeyService.get(apiKey); - const userDto = { + const userDto: UserDTO = { id: api_key.user_id, name: api_key.username, email: api_key.username, diff --git a/src/modules/auth/orchest-identity.integration.spec.ts b/src/modules/auth/orchest-identity.integration.spec.ts index bfe5a29..ab694ef 100644 --- a/src/modules/auth/orchest-identity.integration.spec.ts +++ b/src/modules/auth/orchest-identity.integration.spec.ts @@ -1,17 +1,19 @@ import { deriveOrchestIdentity } from './orchest-identity'; // The three getMe branches must all yield the three fields. This test -// pins the SHAPE contract without loading the DUC gRPC client. +// pins the SHAPE contract without loading the DUC gRPC client. The JWT +// permissions claim carries numeric seqids at runtime (34 = users:admin, +// 31 = intelligence:open), so fixtures here use numeric seqid arrays. describe('/auth/me field contract', () => { - it('cookie/refresh path derives from payload permissions', () => { + it('cookie/refresh path derives from payload permissions (numeric seqids)', () => { const enriched = { id: 'u', name: 'n', email: 'e', customer: { id: 'c', name: 'cust', tier: 't' }, - ...deriveOrchestIdentity(['users:admin', 'intelligence:open']), + ...deriveOrchestIdentity([34, 31]), }; expect(enriched.roles).toContain('super-admin'); expect(enriched.modules).toContain('intelligence'); - expect(enriched.permissions).toHaveLength(2); + expect(enriched.permissions).toEqual(['users:admin', 'intelligence:open']); }); it('api-key branch is empty for all three fields', () => { diff --git a/src/modules/auth/orchest-identity.spec.ts b/src/modules/auth/orchest-identity.spec.ts index c25ceaf..dbaa3fe 100644 --- a/src/modules/auth/orchest-identity.spec.ts +++ b/src/modules/auth/orchest-identity.spec.ts @@ -1,29 +1,37 @@ import { deriveOrchestIdentity } from './orchest-identity'; +// JWT permissions claim carries numeric seqids (not claim strings) — see +// src/decorators/authentication.decorator.ts and +// src/authentication/authentication.guard.ts. 34 = users:admin, +// 31 = intelligence:open, 43 = process:open. describe('deriveOrchestIdentity', () => { - it('passes permissions through verbatim', () => { - expect(deriveOrchestIdentity(['intelligence:open']).permissions).toEqual([ + it('translates seqids to claim strings in permissions', () => { + expect(deriveOrchestIdentity([31]).permissions).toEqual([ 'intelligence:open', ]); }); - it('marks super-admin from users:admin', () => { - expect(deriveOrchestIdentity(['users:admin']).roles).toContain('super-admin'); + it('marks super-admin from the users:admin seqid (34)', () => { + expect(deriveOrchestIdentity([34]).roles).toContain('super-admin'); }); - it('omits super-admin when users:admin absent', () => { - expect(deriveOrchestIdentity(['intelligence:open']).roles).not.toContain( - 'super-admin', - ); + it('omits super-admin when the admin seqid is absent', () => { + expect(deriveOrchestIdentity([31]).roles).not.toContain('super-admin'); }); - it('maps intelligence:open and process:open to module keys', () => { - const { modules } = deriveOrchestIdentity(['intelligence:open', 'process:open']); + it('maps intelligence and process seqids to module keys', () => { + const { modules } = deriveOrchestIdentity([31, 43]); expect(modules.sort()).toEqual(['intelligence', 'process']); }); - it('ignores non-module permissions in modules', () => { - expect(deriveOrchestIdentity(['users:admin']).modules).toEqual([]); + it('ignores non-module seqids in modules', () => { + expect(deriveOrchestIdentity([34]).modules).toEqual([]); + }); + + it('handles a mixed real-shape seqid array (admin + intelligence)', () => { + const { roles, modules } = deriveOrchestIdentity([34, 31]); + expect(roles).toEqual(['super-admin']); + expect(modules).toEqual(['intelligence']); }); it('handles undefined/empty permissions', () => { diff --git a/src/modules/auth/orchest-identity.ts b/src/modules/auth/orchest-identity.ts index 8060e34..38d90eb 100644 --- a/src/modules/auth/orchest-identity.ts +++ b/src/modules/auth/orchest-identity.ts @@ -1,11 +1,29 @@ -// Claim → semantic strings. Seqid knowledge stays here, beside the enum -// that owns it (users:admin=34, intelligence:open=31, process:open=43). -const ADMIN_CLAIM = 'users:admin'; -const MODULE_CLAIMS: Record = { - 'intelligence:open': 'intelligence', - 'process:open': 'process', +import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum'; + +// The JWT `permissions` claim is an array of numeric seqids (not claim +// strings) — see src/authentication/authentication.guard.ts and +// src/decorators/authentication.decorator.ts. Seqids are sourced from +// PERMISSIONS_GROUPS (the enum that owns them), not hand-copied here. +const ADMIN_SEQID = PERMISSIONS_GROUPS.USERS.permissions.ADMIN.seqid; + +const MODULE_SEQID_TO_KEY: Record = { + [PERMISSIONS_GROUPS.INTELLIGENCE.permissions.INTELLIGENCE.seqid]: + 'intelligence', + [PERMISSIONS_GROUPS.PROCESS.permissions.TRANSFORMATION.seqid]: 'process', }; +// Full seqid -> claim catalog, built once from every group/permission in +// PERMISSIONS_GROUPS, so the `permissions` field can be translated back to +// honest claim strings for UserDTO. +const SEQID_TO_CLAIM: Record = Object.values( + PERMISSIONS_GROUPS, +).reduce((catalog, group) => { + for (const permission of Object.values(group.permissions)) { + catalog[permission.seqid] = permission.claim; + } + return catalog; +}, {} as Record); + export type OrchestIdentityFields = { permissions: string[]; roles: string[]; @@ -13,15 +31,24 @@ export type OrchestIdentityFields = { }; export function deriveOrchestIdentity( - permissions: string[] | undefined, + permissions: number[] | undefined, ): OrchestIdentityFields { - const perms = permissions ?? []; + const seqids = permissions ?? []; + const roles: string[] = []; - if (perms.includes(ADMIN_CLAIM)) { + if (seqids.includes(ADMIN_SEQID)) { roles.push('super-admin'); } - const modules = Object.entries(MODULE_CLAIMS) - .filter(([claim]) => perms.includes(claim)) - .map(([, key]) => key); - return { permissions: perms, roles, modules }; + + const modules = seqids + .filter((seqid) => seqid in MODULE_SEQID_TO_KEY) + .map((seqid) => MODULE_SEQID_TO_KEY[seqid]); + + // Unknown seqids (not in the catalog) are dropped: auth-server ignores + // permissions[] in v1, so completeness of this translation isn't required. + const translatedPermissions = seqids + .filter((seqid) => seqid in SEQID_TO_CLAIM) + .map((seqid) => SEQID_TO_CLAIM[seqid]); + + return { permissions: translatedPermissions, roles, modules }; }