diff --git a/.changeset/gentle-llamas-press.md b/.changeset/gentle-llamas-press.md new file mode 100644 index 0000000000..9c922d396f --- /dev/null +++ b/.changeset/gentle-llamas-press.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-defaults': patch +'@backstage/backend-test-utils': minor +--- + +Add a standard `toString` on credentials objects diff --git a/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts b/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts index 8454aa3ecc..f54062efba 100644 --- a/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts @@ -42,6 +42,23 @@ describe('credentials', () => { }, }); + expect( + createCredentialsWithUserPrincipal( + 'user:default/mock', + 'my-token', + undefined, + 'my-actor', + ), + ).toEqual({ + $$type: '@backstage/BackstageCredentials', + version: 'v1', + principal: { + type: 'user', + userEntityRef: 'user:default/mock', + actor: { type: 'service', subject: 'my-actor' }, + }, + }); + expect(createCredentialsWithNonePrincipal()).toEqual({ $$type: '@backstage/BackstageCredentials', version: 'v1', @@ -64,4 +81,34 @@ 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"}"`, + ); + expect( + String( + createCredentialsWithUserPrincipal('user:default/mock', 'my-token'), + ), + ).toMatchInlineSnapshot( + `"{"$$type":"@backstage/BackstageCredentials","type":"user","userEntityRef":"user:default/mock"}"`, + ); + 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"}}"`, + ); + expect(String(createCredentialsWithNonePrincipal())).toMatchInlineSnapshot( + `"{"$$type":"@backstage/BackstageCredentials","type":"none"}"`, + ); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/helpers.ts index 58e2bcff74..0490a1c5fc 100644 --- a/packages/backend-defaults/src/entrypoints/auth/helpers.ts +++ b/packages/backend-defaults/src/entrypoints/auth/helpers.ts @@ -28,23 +28,31 @@ export function createCredentialsWithServicePrincipal( token?: string, accessRestrictions?: BackstagePrincipalAccessRestrictions, ): InternalBackstageCredentials { - return Object.defineProperty( - { - $$type: '@backstage/BackstageCredentials', - version: 'v1', - principal: { + const result = { + $$type: '@backstage/BackstageCredentials', + version: 'v1', + principal: { + type: 'service', + subject: sub, + accessRestrictions, + }, + } 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, - accessRestrictions, - }, - }, - 'token', - { - enumerable: false, - configurable: true, - value: token, - }, - ); + }), + }); + return result; } export function createCredentialsWithUserPrincipal( @@ -53,36 +61,57 @@ export function createCredentialsWithUserPrincipal( expiresAt?: Date, actor?: string, ): InternalBackstageCredentials { - return Object.defineProperty( - { - $$type: '@backstage/BackstageCredentials', - version: 'v1', - expiresAt, - principal: { + const result = { + $$type: '@backstage/BackstageCredentials', + version: 'v1', + expiresAt, + principal: { + type: 'user', + userEntityRef: sub, + ...(actor && { + actor: { type: 'service', subject: actor } as const, + }), + }, + } 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 }, }), - }, - }, - 'token', - { - enumerable: false, - configurable: true, - value: token, - }, - ); + }), + }); + return result; } export function createCredentialsWithNonePrincipal(): InternalBackstageCredentials { - return { + const result = { $$type: '@backstage/BackstageCredentials', version: 'v1', principal: { type: 'none', }, - }; + } as const; + Object.defineProperty(result, 'toString', { + enumerable: false, + configurable: true, + value: () => + JSON.stringify({ + $$type: '@backstage/BackstageCredentials', + type: 'none', + }), + }); + return result; } export function toInternalBackstageCredentials( diff --git a/packages/backend-test-utils/src/services/mockCredentials.test.ts b/packages/backend-test-utils/src/services/mockCredentials.test.ts index 343d6d6d20..f81ec1909c 100644 --- a/packages/backend-test-utils/src/services/mockCredentials.test.ts +++ b/packages/backend-test-utils/src/services/mockCredentials.test.ts @@ -170,4 +170,27 @@ describe('mockCredentials', () => { "Invalid user entity reference 'wrong', expected :/", ); }); + + it('should have a serializable form', () => { + expect(String(mockCredentials.service('my-service'))).toMatchInlineSnapshot( + `"{"$$type":"@backstage/MockBackstageCredentials","type":"service","subject":"my-service"}"`, + ); + expect( + String(mockCredentials.user('user:default/mock')), + ).toMatchInlineSnapshot( + `"{"$$type":"@backstage/MockBackstageCredentials","type":"user","userEntityRef":"user:default/mock"}"`, + ); + expect( + String( + mockCredentials.user('user:default/mock', { + actor: { subject: 'my-actor' }, + }), + ), + ).toMatchInlineSnapshot( + `"{"$$type":"@backstage/MockBackstageCredentials","type":"user","userEntityRef":"user:default/mock","actor":{"type":"service","subject":"my-actor"}}"`, + ); + expect(String(mockCredentials.none())).toMatchInlineSnapshot( + `"{"$$type":"@backstage/MockBackstageCredentials","type":"none"}"`, + ); + }); }); diff --git a/packages/backend-test-utils/src/services/mockCredentials.ts b/packages/backend-test-utils/src/services/mockCredentials.ts index 617ffdb240..6ea57737d7 100644 --- a/packages/backend-test-utils/src/services/mockCredentials.ts +++ b/packages/backend-test-utils/src/services/mockCredentials.ts @@ -76,10 +76,20 @@ export namespace mockCredentials { * Creates a mocked credentials object for a unauthenticated principal. */ export function none(): BackstageCredentials { - return { + const result = { $$type: '@backstage/BackstageCredentials', principal: { type: 'none' }, - }; + } as const; + Object.defineProperty(result, 'toString', { + enumerable: false, + configurable: true, + value: () => + JSON.stringify({ + $$type: '@backstage/MockBackstageCredentials', + type: 'none', + }), + }); + return result; } /** @@ -111,24 +121,35 @@ export namespace mockCredentials { options?: { actor?: { subject: string } }, ): BackstageCredentials { validateUserEntityRef(userEntityRef); - return Object.defineProperty( - { - $$type: '@backstage/BackstageCredentials', - principal: { + const result = { + $$type: '@backstage/BackstageCredentials', + principal: { + type: 'user', + userEntityRef, + ...(options?.actor && { + actor: { type: 'service', subject: options.actor.subject } as const, + }), + }, + } 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 }, }), - }, - }, - 'token', - { - enumerable: false, - configurable: true, - value: user.token(), - }, - ); + }), + }); + Object.defineProperty(result, 'token', { + enumerable: false, + configurable: true, + value: user.token(), + }); + return result; } /** @@ -231,14 +252,25 @@ export namespace mockCredentials { subject: string = DEFAULT_MOCK_SERVICE_SUBJECT, accessRestrictions?: BackstagePrincipalAccessRestrictions, ): BackstageCredentials { - return { + const result = { $$type: '@backstage/BackstageCredentials', principal: { type: 'service', subject, ...(accessRestrictions ? { accessRestrictions } : {}), }, - }; + } as const; + Object.defineProperty(result, 'toString', { + enumerable: false, + configurable: true, + value: () => + JSON.stringify({ + $$type: '@backstage/MockBackstageCredentials', + type: 'service', + subject, + }), + }); + return result; } /**