diff --git a/backend/src/api/public/v1/akrites-external/index.ts b/backend/src/api/public/v1/akrites-external/index.ts index 1fd5a53dc0..00c245e244 100644 --- a/backend/src/api/public/v1/akrites-external/index.ts +++ b/backend/src/api/public/v1/akrites-external/index.ts @@ -51,49 +51,50 @@ const contactIngestRateLimiter = envTunableRateLimiter( export function akritesExternalRouter(): Router { const router = Router() - // TODO: swap for a dedicated cdp:packages:read scope once Akrites gets its own - // Auth0 M2M scopes (per the akrites-external draft contract) — reusing the - // internal CDP UI scopes for now since that's what's actually issued today. + // Any one of the dedicated Akrites scope or the old Self Serve scopes works for now — + // drop READ_PACKAGES/READ_STEWARDSHIPS once Akrites cuts over. const packagesSubRouter = Router() packagesSubRouter.use(rateLimiter) - packagesSubRouter.use(requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all')) + packagesSubRouter.use( + requireScopes( + [SCOPES.READ_AKRITES_PACKAGES, SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], + 'any', + ), + ) packagesSubRouter.get('/detail', safeWrap(getAkritesExternalPackageDetail)) packagesSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalPackageDetailBatch)) router.use('/packages', packagesSubRouter) - // TODO: the contract gates advisories behind a dedicated read:advisories scope - // (see the scope-naming note in the akrites-external OpenAPI). That scope isn't - // issued by Auth0 yet, so reuse READ_PACKAGES for now — advisories are package - // security data and, unlike the packages endpoints above, need no stewardship read. + // Dedicated read:akrites-advisories, or Self Serve's read:packages as a + // fallback until Akrites cuts over — drop it then. + const advisoriesScopes = [SCOPES.READ_PACKAGES, SCOPES.READ_AKRITES_ADVISORIES] const advisoriesSubRouter = Router() advisoriesSubRouter.use(rateLimiter) - advisoriesSubRouter.use(requireScopes([SCOPES.READ_PACKAGES])) + advisoriesSubRouter.use(requireScopes(advisoriesScopes, 'any')) advisoriesSubRouter.get('/detail', safeWrap(getAkritesExternalAdvisoryDetail)) advisoriesSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalAdvisoryDetailBatch)) router.use('/advisories', advisoriesSubRouter) - // Security contacts expose contact PII (e.g. reporter emails), so the contract gates - // them behind a dedicated cdp:maintainers:read scope and explicitly forbids reaching - // them via the packages scope. That scope isn't issued by Auth0 yet, so reuse the - // closest issued one — READ_MAINTAINER_ROLES (maintainer data) — NOT READ_PACKAGES. - // TODO: swap for cdp:maintainers:read once issued. + // Contact PII stays behind a dedicated scope, never the packages scope: dedicated + // read:akrites-maintainers, or Self Serve's read:maintainer-roles as a fallback. + // // requireScopes is applied per-route (not router-level) so each route can put its own // rate limiter *before* the scope check — failed-auth requests still count against that // route's quota — without forcing every route in this subrouter onto the same limiter // instance. /ingest gets its own dedicated contactIngestRateLimiter instead of sharing // the read endpoints' quota, matching the blast-radius jobs endpoint below. - const contactsScopes = [SCOPES.READ_MAINTAINER_ROLES] + const contactsScopes = [SCOPES.READ_MAINTAINER_ROLES, SCOPES.READ_AKRITES_MAINTAINERS] const contactsSubRouter = Router() contactsSubRouter.get( '/detail', rateLimiter, - requireScopes(contactsScopes), + requireScopes(contactsScopes, 'any'), safeWrap(getAkritesExternalContactDetail), ) contactsSubRouter.post( /^\/detail:batch\/?$/, rateLimiter, - requireScopes(contactsScopes), + requireScopes(contactsScopes, 'any'), safeWrap(getAkritesExternalContactDetailBatch), ) // Sync, single-purl on-demand ingest — starts a Temporal workflow and blocks a while, @@ -101,16 +102,15 @@ export function akritesExternalRouter(): Router { contactsSubRouter.post( '/ingest', contactIngestRateLimiter, - requireScopes(contactsScopes), + requireScopes(contactsScopes, 'any'), safeWrap(ingestAkritesExternalContactDetail), ) router.use('/contacts', contactsSubRouter) - // TODO: the contract gates blast-radius behind a dedicated read:advisories scope - // (same as advisories above — see the scope-naming note in the akrites-external - // OpenAPI). Not issued by Auth0 yet, so reuse READ_PACKAGES for now. + // Same underlying data as advisories above, same scopes: read:akrites-advisories, + // or Self Serve's read:packages as a fallback until Akrites cuts over. const blastRadiusSubRouter = Router() - blastRadiusSubRouter.use(requireScopes([SCOPES.READ_PACKAGES])) + blastRadiusSubRouter.use(requireScopes(advisoriesScopes, 'any')) blastRadiusSubRouter.post('/jobs', blastRadiusRateLimiter, safeWrap(submitBlastRadiusJob)) // Bulk submit multiplies Temporal workflow starts per request (up to // MAX_BLAST_RADIUS_JOBS_PER_BATCH), so it sits behind the same strict diff --git a/backend/src/api/public/v1/akrites-external/openapi.yaml b/backend/src/api/public/v1/akrites-external/openapi.yaml index 8dd904231a..034e53c766 100644 --- a/backend/src/api/public/v1/akrites-external/openapi.yaml +++ b/backend/src/api/public/v1/akrites-external/openapi.yaml @@ -16,38 +16,47 @@ info: 7-day result cache is specced separately and not yet built. - TODO: scopes below (read:packages, read:stewardships) are the existing - internal CDP UI scopes, reused here for now. Swap for a dedicated - cdp:packages:read scope once Akrites gets its own Auth0 M2M scopes per - the akrites-external draft contract. + Auth0 now issues a dedicated Akrites Enclave M2M client with its own + read:akrites-packages / read:akrites-advisories / read:akrites-maintainers + scopes. Until Akrites cuts over to that client's credentials, the Self Serve + client's existing scopes (read:packages, read:stewardships, + read:maintainer-roles) still work on every operation below — each operation + lists both the new dedicated scope(s) and the old fallback scope(s) as + alternatives. Drop the old alternative once the cutover is confirmed. servers: - url: https://cm.lfx.dev/api/v1 description: Production security: + - M2MBearer: + - read:akrites-packages - M2MBearer: - read:packages + - M2MBearer: - read:stewardships tags: - name: Packages - description: Package detail — requires read:packages and read:stewardships (see TODO above). + description: > + Package detail — requires read:akrites-packages (dedicated Akrites Enclave + scope), or either read:packages or read:stewardships from the old Self Serve + client as a fallback until the cutover completes. - name: Advisories description: > - Security advisories for a package, split out of package detail. The draft - contract gates these behind a dedicated read:advisories scope; until Auth0 - issues it, the implementation reuses read:packages (advisories need no - stewardship read). Confirm the final scope name (read:advisories vs - cdp:advisories:read) with Akrites/product. + Security advisories for a package, split out of package detail. Requires + read:akrites-advisories (dedicated Akrites Enclave scope), or read:packages + from the old Self Serve client as a fallback until the cutover completes + (advisories need no stewardship read). - name: Contacts description: > Security contacts for a package — includes contact PII (e.g. reporter - emails). The contract gates these behind a dedicated cdp:maintainers:read - scope and forbids reaching them via the packages scope; until Auth0 issues - it, the implementation requires read:maintainer-roles (NOT read:packages). - The response shape is still under discussion upstream (reportingMethods / - reportingGuidelines / integrationHints are reserved and always null today). + emails). Requires read:akrites-maintainers (dedicated Akrites Enclave + scope), or read:maintainer-roles from the old Self Serve client as a + fallback until the cutover completes — never reachable via the packages + scopes. The response shape is still under discussion upstream + (reportingMethods / reportingGuidelines / integrationHints are reserved + and always null today). - name: Blast Radius description: > Advisory reachability analysis — submit (2a) and poll (2b) are both @@ -59,14 +68,14 @@ tags: entry starts its own workflow — and stays behind the same strict rate limiter as the single-job route; bulk poll (jobs:batch/poll) is read-only and capped at 100 like the other batch - endpoints. Same interim scope note as Advisories applies (read:packages, - pending a dedicated read:advisories scope). + endpoints. Same scope note as Advisories applies (read:akrites-advisories, + or read:packages as a fallback until the cutover completes). components: securitySchemes: # Modeled as oauth2/clientCredentials (not http/bearer): OpenAPI only allows # non-empty scope arrays in security requirements for oauth2 / openIdConnect - # schemes, and the operations below require read:packages + read:stewardships. + # schemes. M2MBearer: type: oauth2 description: > @@ -77,9 +86,12 @@ components: clientCredentials: tokenUrl: https://linuxfoundation.auth0.com/oauth/token scopes: - read:packages: Read package detail - read:stewardships: Read package stewardship data - read:maintainer-roles: Read security contacts (interim scope for Contacts; see the Contacts tag) + read:akrites-packages: Read package detail (dedicated Akrites Enclave scope) + read:akrites-advisories: Read security advisories and submit/poll blast-radius jobs (dedicated Akrites Enclave scope) + read:akrites-maintainers: Read security contacts (dedicated Akrites Enclave scope) + read:packages: Read package detail (fallback — old Self Serve client scope, being phased out) + read:stewardships: Read package stewardship data (fallback — old Self Serve client scope, being phased out) + read:maintainer-roles: Read security contacts (fallback — old Self Serve client scope, being phased out) schemas: Error: @@ -853,8 +865,11 @@ paths: summary: Get package detail by PURL tags: [Packages] security: + - M2MBearer: + - read:akrites-packages - M2MBearer: - read:packages + - M2MBearer: - read:stewardships parameters: - name: purl @@ -883,7 +898,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:packages or read:stewardships scope. + description: Token missing read:akrites-packages, read:packages, or read:stewardships scope. content: application/json: schema: @@ -901,8 +916,11 @@ paths: summary: Bulk package detail lookup tags: [Packages] security: + - M2MBearer: + - read:akrites-packages - M2MBearer: - read:packages + - M2MBearer: - read:stewardships requestBody: required: true @@ -960,7 +978,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:packages or read:stewardships scope. + description: Token missing read:akrites-packages, read:packages, or read:stewardships scope. content: application/json: schema: @@ -972,6 +990,8 @@ paths: summary: Get advisories for a package by PURL tags: [Advisories] security: + - M2MBearer: + - read:akrites-advisories - M2MBearer: - read:packages parameters: @@ -1001,7 +1021,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:packages scope. + description: Token missing read:akrites-advisories, or read:packages, scope. content: application/json: schema: @@ -1019,6 +1039,8 @@ paths: summary: Bulk advisory lookup tags: [Advisories] security: + - M2MBearer: + - read:akrites-advisories - M2MBearer: - read:packages requestBody: @@ -1077,7 +1099,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:packages scope. + description: Token missing read:akrites-advisories, or read:packages, scope. content: application/json: schema: @@ -1099,6 +1121,8 @@ paths: Not yet implemented: the 7-day result cache and force-bypass semantics. tags: [Blast Radius] security: + - M2MBearer: + - read:akrites-advisories - M2MBearer: - read:packages requestBody: @@ -1131,7 +1155,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:packages scope. + description: Token missing read:akrites-advisories, or read:packages, scope. content: application/json: schema: @@ -1160,6 +1184,8 @@ paths: back with status: 'failed' and the rest still submit. tags: [Blast Radius] security: + - M2MBearer: + - read:akrites-advisories - M2MBearer: - read:packages requestBody: @@ -1188,7 +1214,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:packages scope. + description: Token missing read:akrites-advisories, or read:packages, scope. content: application/json: schema: @@ -1212,6 +1238,8 @@ paths: submit limiter. tags: [Blast Radius] security: + - M2MBearer: + - read:akrites-advisories - M2MBearer: - read:packages requestBody: @@ -1253,7 +1281,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:packages scope. + description: Token missing read:akrites-advisories, or read:packages, scope. content: application/json: schema: @@ -1269,6 +1297,8 @@ paths: status. tags: [Blast Radius] security: + - M2MBearer: + - read:akrites-advisories - M2MBearer: - read:packages parameters: @@ -1298,7 +1328,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:packages scope. + description: Token missing read:akrites-advisories, or read:packages, scope. content: application/json: schema: @@ -1321,10 +1351,12 @@ paths: operationId: getContactDetail summary: Get security contact detail by PURL description: > - Requires read:maintainer-roles (interim for the contract's cdp:maintainers:read). - Never reachable with the packages scope. + Requires read:akrites-maintainers, or read:maintainer-roles from the old Self + Serve client as a fallback. Never reachable with the packages scope. tags: [Contacts] security: + - M2MBearer: + - read:akrites-maintainers - M2MBearer: - read:maintainer-roles parameters: @@ -1354,7 +1386,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:maintainer-roles scope. + description: Token missing read:akrites-maintainers, or read:maintainer-roles, scope. content: application/json: schema: @@ -1372,6 +1404,8 @@ paths: summary: Bulk security contact lookup tags: [Contacts] security: + - M2MBearer: + - read:akrites-maintainers - M2MBearer: - read:maintainer-roles requestBody: @@ -1430,7 +1464,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:maintainer-roles scope. + description: Token missing read:akrites-maintainers, or read:maintainer-roles, scope. content: application/json: schema: @@ -1472,6 +1506,8 @@ paths: so callers aren't held open for the duration of the ingest. tags: [Contacts] security: + - M2MBearer: + - read:akrites-maintainers - M2MBearer: - read:maintainer-roles requestBody: @@ -1507,7 +1543,7 @@ paths: schema: $ref: '#/components/schemas/Error' '403': - description: Token missing read:maintainer-roles scope. + description: Token missing read:akrites-maintainers, or read:maintainer-roles, scope. content: application/json: schema: diff --git a/backend/src/security/scopes.ts b/backend/src/security/scopes.ts index 5f7bd77f01..1118767d16 100644 --- a/backend/src/security/scopes.ts +++ b/backend/src/security/scopes.ts @@ -14,6 +14,9 @@ export const SCOPES = { READ_PACKAGES: 'read:packages', READ_STEWARDSHIPS: 'read:stewardships', WRITE_STEWARDSHIPS: 'write:stewardships', + READ_AKRITES_PACKAGES: 'read:akrites-packages', + READ_AKRITES_ADVISORIES: 'read:akrites-advisories', + READ_AKRITES_MAINTAINERS: 'read:akrites-maintainers', } as const export type Scope = (typeof SCOPES)[keyof typeof SCOPES]