use defineProperties

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2025-06-12 16:02:26 +02:00
parent ead925a8a2
commit a098fa73da
4 changed files with 203 additions and 116 deletions
@@ -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"}}"`,
);
});
});
@@ -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<BackstageServicePrincipal> {
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<BackstageUserPrincipal> {
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<BackstageNonePrincipal> {
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;
}
@@ -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}"`,
);
});
});
@@ -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;
}