From cd21fd0b7bbaed25114b4759c71d6b73ceae99c1 Mon Sep 17 00:00:00 2001 From: Rafael Date: Mon, 24 Aug 2026 16:30:33 -0300 Subject: [PATCH] refactor(auth): /auth/me returns permissions only (drop roles/modules) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep Maestro a pure identity provider: /auth/me exposes the user's permission claim strings and nothing consumer-specific. Consumers derive whatever meaning they need (roles, module access, groups) from the claim vocabulary — claims are already namespaced group:action. - UserDTO: drop roles[]/modules[], keep permissions[]. - Helper shrinks to a generic seqid->claim translation (orchest-identity.ts -> permission-claims.ts, translateSeqidsToClaims). - api-key branch: permissions: [] only. The roles/modules derivation moves entirely to the consumer (Orchest's auth-server adapter). Co-Authored-By: WOZCODE --- src/modules/auth/auth.controller.ts | 2 - src/modules/auth/auth.service.ts | 4 +- src/modules/auth/dtos/login.ts | 2 - .../auth/orchest-identity.integration.spec.ts | 29 ---------- src/modules/auth/orchest-identity.spec.ts | 44 --------------- src/modules/auth/orchest-identity.ts | 54 ------------------- src/modules/auth/permission-claims.spec.ts | 30 +++++++++++ src/modules/auth/permission-claims.ts | 29 ++++++++++ 8 files changed, 61 insertions(+), 133 deletions(-) delete mode 100644 src/modules/auth/orchest-identity.integration.spec.ts delete mode 100644 src/modules/auth/orchest-identity.spec.ts delete mode 100644 src/modules/auth/orchest-identity.ts create mode 100644 src/modules/auth/permission-claims.spec.ts create mode 100644 src/modules/auth/permission-claims.ts diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index e88cc0f..5eb2b0f 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -497,8 +497,6 @@ export class AuthController { tier: api_key.customer_tier, }, permissions: [], - roles: [], - modules: [], }; return res.status(200).json(userDto); diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index e0f6ba8..a7d9868 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -35,7 +35,7 @@ import { BulkEditResponse, UserDTO } from './dtos/login'; import jwt, { JwtPayload } from 'jsonwebtoken'; import { PackTheMetadata } from 'src/utils/PackTheMetadata'; import { Request, Response } from 'express'; -import { deriveOrchestIdentity } from './orchest-identity'; +import { translateSeqidsToClaims } from './permission-claims'; type AuthSession = { accessToken?: string; @@ -448,7 +448,7 @@ export class AuthClientService implements OnModuleInit { name: payload.customer_name, tier: payload.customer_tier, }, - ...deriveOrchestIdentity(payload.permissions), + permissions: translateSeqidsToClaims(payload.permissions), }; return userDto; diff --git a/src/modules/auth/dtos/login.ts b/src/modules/auth/dtos/login.ts index 8532cb1..3317904 100644 --- a/src/modules/auth/dtos/login.ts +++ b/src/modules/auth/dtos/login.ts @@ -154,6 +154,4 @@ export type UserDTO = { tier: string, }, permissions: string[], - roles: string[], - modules: string[], } diff --git a/src/modules/auth/orchest-identity.integration.spec.ts b/src/modules/auth/orchest-identity.integration.spec.ts deleted file mode 100644 index ab694ef..0000000 --- a/src/modules/auth/orchest-identity.integration.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -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. 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 (numeric seqids)', () => { - const enriched = { - id: 'u', name: 'n', email: 'e', - customer: { id: 'c', name: 'cust', tier: 't' }, - ...deriveOrchestIdentity([34, 31]), - }; - expect(enriched.roles).toContain('super-admin'); - expect(enriched.modules).toContain('intelligence'); - expect(enriched.permissions).toEqual(['users:admin', 'intelligence:open']); - }); - - it('api-key branch is empty for all three fields', () => { - const apiKeyDto = { - id: 'u', name: 'n', email: 'n', - customer: { id: 'c', name: 'cust', tier: 't' }, - permissions: [] as string[], roles: [] as string[], modules: [] as string[], - }; - expect(apiKeyDto.permissions).toEqual([]); - expect(apiKeyDto.roles).toEqual([]); - expect(apiKeyDto.modules).toEqual([]); - }); -}); diff --git a/src/modules/auth/orchest-identity.spec.ts b/src/modules/auth/orchest-identity.spec.ts deleted file mode 100644 index dbaa3fe..0000000 --- a/src/modules/auth/orchest-identity.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -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('translates seqids to claim strings in permissions', () => { - expect(deriveOrchestIdentity([31]).permissions).toEqual([ - 'intelligence:open', - ]); - }); - - it('marks super-admin from the users:admin seqid (34)', () => { - expect(deriveOrchestIdentity([34]).roles).toContain('super-admin'); - }); - - it('omits super-admin when the admin seqid is absent', () => { - expect(deriveOrchestIdentity([31]).roles).not.toContain('super-admin'); - }); - - it('maps intelligence and process seqids to module keys', () => { - const { modules } = deriveOrchestIdentity([31, 43]); - expect(modules.sort()).toEqual(['intelligence', 'process']); - }); - - 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', () => { - expect(deriveOrchestIdentity(undefined)).toEqual({ - permissions: [], - roles: [], - modules: [], - }); - }); -}); diff --git a/src/modules/auth/orchest-identity.ts b/src/modules/auth/orchest-identity.ts deleted file mode 100644 index 38d90eb..0000000 --- a/src/modules/auth/orchest-identity.ts +++ /dev/null @@ -1,54 +0,0 @@ -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[]; - modules: string[]; -}; - -export function deriveOrchestIdentity( - permissions: number[] | undefined, -): OrchestIdentityFields { - const seqids = permissions ?? []; - - const roles: string[] = []; - if (seqids.includes(ADMIN_SEQID)) { - roles.push('super-admin'); - } - - 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 }; -} diff --git a/src/modules/auth/permission-claims.spec.ts b/src/modules/auth/permission-claims.spec.ts new file mode 100644 index 0000000..c528c5b --- /dev/null +++ b/src/modules/auth/permission-claims.spec.ts @@ -0,0 +1,30 @@ +import { translateSeqidsToClaims } from './permission-claims'; + +// The JWT permissions claim carries numeric seqids at runtime +// (34 = users:admin, 31 = intelligence:open, 43 = process:open), so +// fixtures here use numeric seqid arrays. +describe('translateSeqidsToClaims', () => { + it('translates a single seqid to its claim string', () => { + expect(translateSeqidsToClaims([31])).toEqual(['intelligence:open']); + }); + + it('translates multiple seqids, preserving order', () => { + expect(translateSeqidsToClaims([34, 31, 43])).toEqual([ + 'users:admin', + 'intelligence:open', + 'process:open', + ]); + }); + + it('drops unknown seqids not present in the catalog', () => { + expect(translateSeqidsToClaims([34, 999999])).toEqual(['users:admin']); + }); + + it('returns an empty array for undefined permissions', () => { + expect(translateSeqidsToClaims(undefined)).toEqual([]); + }); + + it('returns an empty array for empty permissions', () => { + expect(translateSeqidsToClaims([])).toEqual([]); + }); +}); diff --git a/src/modules/auth/permission-claims.ts b/src/modules/auth/permission-claims.ts new file mode 100644 index 0000000..470631e --- /dev/null +++ b/src/modules/auth/permission-claims.ts @@ -0,0 +1,29 @@ +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. This module translates those +// seqids into their human-readable claim strings so /auth/me can expose a +// self-describing `permissions` array; consumers derive whatever meaning +// they need (roles, module access, groups) from the claim vocabulary. +// +// Full seqid -> claim catalog, built once from every group/permission in +// PERMISSIONS_GROUPS. Generic: no consumer-specific knowledge lives here. +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); + +// Translate the JWT's numeric permission seqids into claim strings. +// Unknown seqids (not present in the catalog) are dropped. +export function translateSeqidsToClaims( + permissions: number[] | undefined, +): string[] { + return (permissions ?? []) + .filter((seqid) => seqid in SEQID_TO_CLAIM) + .map((seqid) => SEQID_TO_CLAIM[seqid]); +}