diff --git a/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts b/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts index f54062efba..fdc6e26c3a 100644 --- a/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts @@ -82,33 +82,62 @@ describe('credentials', () => { ).not.toMatch(/my-token/); }); - it('should have a serializable form', () => { - expect( - String(createCredentialsWithServicePrincipal('my-service')), - ).toMatchInlineSnapshot( - `"{"$$type":"@backstage/BackstageCredentials","type":"service","subject":"my-service"}"`, + it('should have a serializable form both as strings and as JSON', () => { + const simpleService = createCredentialsWithServicePrincipal('my-service'); + expect(String(simpleService)).toMatchInlineSnapshot( + `"backstageCredentials{servicePrincipal{my-service}}"`, ); - expect( - String( - createCredentialsWithUserPrincipal('user:default/mock', 'my-token'), - ), - ).toMatchInlineSnapshot( - `"{"$$type":"@backstage/BackstageCredentials","type":"user","userEntityRef":"user:default/mock"}"`, + expect(JSON.stringify(simpleService)).toMatchInlineSnapshot( + `"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"service","subject":"my-service"}}"`, ); - expect( - String( - createCredentialsWithUserPrincipal( - 'user:default/mock', - 'my-token', - undefined, - 'my-actor', - ), - ), - ).toMatchInlineSnapshot( - `"{"$$type":"@backstage/BackstageCredentials","type":"user","userEntityRef":"user:default/mock","actor":{"type":"service","subject":"my-actor"}}"`, + + const serviceWithAccessRestrictions = createCredentialsWithServicePrincipal( + 'my-service', + undefined, + { + permissionNames: ['perm'], + permissionAttributes: { + action: ['read'], + }, + }, ); - expect(String(createCredentialsWithNonePrincipal())).toMatchInlineSnapshot( - `"{"$$type":"@backstage/BackstageCredentials","type":"none"}"`, + expect(String(serviceWithAccessRestrictions)).toMatchInlineSnapshot( + `"backstageCredentials{servicePrincipal{my-service,accessRestrictions=cXWOJgUirHkHNZIowUi/YO5nwEwhTicC38iXi2XTYCk}}"`, + ); + expect(JSON.stringify(serviceWithAccessRestrictions)).toMatchInlineSnapshot( + `"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"service","subject":"my-service","accessRestrictions":{"permissionNames":["perm"],"permissionAttributes":{"action":["read"]}}}}"`, + ); + + const simpleUser = createCredentialsWithUserPrincipal( + 'user:default/mock', + 'my-token', + ); + expect(String(simpleUser)).toMatchInlineSnapshot( + `"backstageCredentials{userPrincipal{user:default/mock}}"`, + ); + expect(JSON.stringify(simpleUser)).toMatchInlineSnapshot( + `"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"user","userEntityRef":"user:default/mock"}}"`, + ); + + const userWithActor = createCredentialsWithUserPrincipal( + 'user:default/mock', + 'my-token', + undefined, + 'my-actor', + ); + expect(String(userWithActor)).toMatchInlineSnapshot( + `"backstageCredentials{userPrincipal{user:default/mock,actor={servicePrincipal{my-actor}}}}"`, + ); + expect(JSON.stringify(userWithActor)).toMatchInlineSnapshot( + `"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"user","userEntityRef":"user:default/mock","actor":{"type":"service","subject":"my-actor"}}}"`, + ); + + const none = createCredentialsWithNonePrincipal(); + expect(String(none)).toMatchInlineSnapshot( + `"backstageCredentials{nonePrincipal}"`, + ); + expect(JSON.stringify(none)).toMatchInlineSnapshot( + `"{"$$type":"@backstage/BackstageCredentials","version":"v1","principal":{"type":"none"}}"`, ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/helpers.ts index 0490a1c5fc..02840112a9 100644 --- a/packages/backend-defaults/src/entrypoints/auth/helpers.ts +++ b/packages/backend-defaults/src/entrypoints/auth/helpers.ts @@ -22,35 +22,32 @@ import { BackstageUserPrincipal, } from '@backstage/backend-plugin-api'; import { InternalBackstageCredentials } from './types'; +import { createHash } from 'crypto'; export function createCredentialsWithServicePrincipal( sub: string, token?: string, accessRestrictions?: BackstagePrincipalAccessRestrictions, ): InternalBackstageCredentials { + const principal = createServicePrincipal(sub, accessRestrictions); const result = { $$type: '@backstage/BackstageCredentials', version: 'v1', - principal: { - type: 'service', - subject: sub, - accessRestrictions, - }, + principal, } as const; - Object.defineProperty(result, 'token', { - enumerable: false, - configurable: true, - value: token, - }); - Object.defineProperty(result, 'toString', { - enumerable: false, - configurable: true, - value: () => - JSON.stringify({ - $$type: '@backstage/BackstageCredentials', - type: 'service', - subject: sub, - }), + Object.defineProperties(result, { + token: { + enumerable: false, + configurable: true, + writable: true, + value: token, + }, + toString: { + enumerable: false, + configurable: true, + writable: true, + value: () => `backstageCredentials{${principal}}`, + }, }); return result; } @@ -61,55 +58,47 @@ export function createCredentialsWithUserPrincipal( expiresAt?: Date, actor?: string, ): InternalBackstageCredentials { + const principal = createUserPrincipal( + sub, + actor ? createServicePrincipal(actor) : undefined, + ); const result = { $$type: '@backstage/BackstageCredentials', version: 'v1', expiresAt, - principal: { - type: 'user', - userEntityRef: sub, - ...(actor && { - actor: { type: 'service', subject: actor } as const, - }), - }, + principal, } as const; - Object.defineProperty(result, 'token', { - enumerable: false, - configurable: true, - value: token, - }); - Object.defineProperty(result, 'toString', { - enumerable: false, - configurable: true, - value: () => - JSON.stringify({ - $$type: '@backstage/BackstageCredentials', - type: 'user', - userEntityRef: sub, - ...(actor && { - actor: { type: 'service', subject: actor }, - }), - }), + Object.defineProperties(result, { + token: { + enumerable: false, + configurable: true, + writable: true, + value: token, + }, + toString: { + enumerable: false, + configurable: true, + writable: true, + value: () => `backstageCredentials{${principal}}`, + }, }); return result; } export function createCredentialsWithNonePrincipal(): InternalBackstageCredentials { + const principal = createNonePrincipal(); const result = { $$type: '@backstage/BackstageCredentials', version: 'v1', - principal: { - type: 'none', - }, + principal, } as const; - Object.defineProperty(result, 'toString', { - enumerable: false, - configurable: true, - value: () => - JSON.stringify({ - $$type: '@backstage/BackstageCredentials', - type: 'none', - }), + Object.defineProperties(result, { + toString: { + enumerable: false, + configurable: true, + writable: true, + value: () => `backstageCredentials{${principal}}`, + }, }); return result; } @@ -135,3 +124,74 @@ export function toInternalBackstageCredentials( return internalCredentials; } + +function createServicePrincipal( + sub: string, + accessRestrictions?: BackstagePrincipalAccessRestrictions, +): BackstageServicePrincipal { + const result = { + type: 'service', + subject: sub, + accessRestrictions, + } as const; + Object.defineProperties(result, { + toString: { + enumerable: false, + configurable: true, + writable: true, + value: () => { + let parts = sub; + if (accessRestrictions) { + const hash = createHash('sha256') + .update(JSON.stringify(accessRestrictions)) + .digest('base64') + .replace(/=+$/, ''); + parts += `,accessRestrictions=${hash}`; + } + return `servicePrincipal{${parts}}`; + }, + }, + }); + return result; +} + +function createUserPrincipal( + userEntityRef: string, + actor?: BackstageServicePrincipal, +): BackstageUserPrincipal { + const result = { + type: 'user', + userEntityRef, + actor, + } as const; + Object.defineProperties(result, { + toString: { + enumerable: false, + configurable: true, + writable: true, + value: () => { + let parts = userEntityRef; + if (actor) { + parts += `,actor={${actor}}`; + } + return `userPrincipal{${parts}}`; + }, + }, + }); + return result; +} + +function createNonePrincipal(): BackstageNonePrincipal { + const result = { + type: 'none', + } as const; + Object.defineProperties(result, { + toString: { + enumerable: false, + configurable: true, + writable: true, + value: () => 'nonePrincipal', + }, + }); + return result; +} diff --git a/packages/backend-test-utils/src/services/mockCredentials.test.ts b/packages/backend-test-utils/src/services/mockCredentials.test.ts index f81ec1909c..9248e3bde0 100644 --- a/packages/backend-test-utils/src/services/mockCredentials.test.ts +++ b/packages/backend-test-utils/src/services/mockCredentials.test.ts @@ -173,12 +173,12 @@ describe('mockCredentials', () => { it('should have a serializable form', () => { expect(String(mockCredentials.service('my-service'))).toMatchInlineSnapshot( - `"{"$$type":"@backstage/MockBackstageCredentials","type":"service","subject":"my-service"}"`, + `"mockCredentials{servicePrincipal{my-service}}"`, ); expect( String(mockCredentials.user('user:default/mock')), ).toMatchInlineSnapshot( - `"{"$$type":"@backstage/MockBackstageCredentials","type":"user","userEntityRef":"user:default/mock"}"`, + `"mockCredentials{userPrincipal{user:default/mock}}"`, ); expect( String( @@ -187,10 +187,10 @@ describe('mockCredentials', () => { }), ), ).toMatchInlineSnapshot( - `"{"$$type":"@backstage/MockBackstageCredentials","type":"user","userEntityRef":"user:default/mock","actor":{"type":"service","subject":"my-actor"}}"`, + `"mockCredentials{userPrincipal{user:default/mock,actor={my-actor}}}"`, ); expect(String(mockCredentials.none())).toMatchInlineSnapshot( - `"{"$$type":"@backstage/MockBackstageCredentials","type":"none"}"`, + `"mockCredentials{nonePrincipal}"`, ); }); }); diff --git a/packages/backend-test-utils/src/services/mockCredentials.ts b/packages/backend-test-utils/src/services/mockCredentials.ts index 6ea57737d7..d749f7edb1 100644 --- a/packages/backend-test-utils/src/services/mockCredentials.ts +++ b/packages/backend-test-utils/src/services/mockCredentials.ts @@ -80,14 +80,13 @@ export namespace mockCredentials { $$type: '@backstage/BackstageCredentials', principal: { type: 'none' }, } as const; - Object.defineProperty(result, 'toString', { - enumerable: false, - configurable: true, - value: () => - JSON.stringify({ - $$type: '@backstage/MockBackstageCredentials', - type: 'none', - }), + Object.defineProperties(result, { + toString: { + enumerable: false, + configurable: true, + writable: true, + value: () => `mockCredentials{nonePrincipal}`, + }, }); return result; } @@ -131,23 +130,20 @@ export namespace mockCredentials { }), }, } as const; - Object.defineProperty(result, 'toString', { - enumerable: false, - configurable: true, - value: () => - JSON.stringify({ - $$type: '@backstage/MockBackstageCredentials', - type: 'user', - userEntityRef, - ...(options?.actor && { - actor: { type: 'service', subject: options.actor.subject }, - }), - }), - }); - Object.defineProperty(result, 'token', { - enumerable: false, - configurable: true, - value: user.token(), + Object.defineProperties(result, { + toString: { + enumerable: false, + configurable: true, + value: () => + `mockCredentials{userPrincipal{${userEntityRef}${ + options?.actor ? `,actor={${options.actor.subject}}` : '' + }}}`, + }, + token: { + enumerable: false, + configurable: true, + value: user.token(), + }, }); return result; } @@ -260,15 +256,17 @@ export namespace mockCredentials { ...(accessRestrictions ? { accessRestrictions } : {}), }, } as const; - Object.defineProperty(result, 'toString', { - enumerable: false, - configurable: true, - value: () => - JSON.stringify({ - $$type: '@backstage/MockBackstageCredentials', - type: 'service', - subject, - }), + Object.defineProperties(result, { + toString: { + enumerable: false, + configurable: true, + value: () => + `mockCredentials{servicePrincipal{${subject}${ + accessRestrictions + ? `,accessRestrictions=${JSON.stringify(accessRestrictions)}` + : '' + }}}`, + }, }); return result; }