backend-test-utils: refactor mock service auth tokens to mirror creation options

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-02-17 18:00:42 +01:00
parent caf1af6541
commit faf5190d73
6 changed files with 138 additions and 105 deletions
+27 -12
View File
@@ -49,15 +49,22 @@ export function isDockerDisabledForTests(): boolean;
// @public (undocumented)
export namespace mockCredentials {
export function none(): BackstageCredentials<BackstageNonePrincipal>;
export namespace none {
export function header(): string;
}
export function service(
subject?: string,
): BackstageCredentials<BackstageServicePrincipal>;
export namespace service {
export function header(payload?: TokenPayload): string;
export function token(payload?: TokenPayload): string;
export type TokenPayload = {
subject?: string;
targetPluginId?: string;
export function header(options?: TokenOptions): string;
// (undocumented)
export function invalidHeader(): string;
// (undocumented)
export function invalidToken(): string;
export function token(options?: TokenOptions): string;
export type TokenOptions = {
onBehalfOf: BackstageCredentials;
targetPluginId: string;
};
}
export function user(
@@ -65,10 +72,11 @@ export namespace mockCredentials {
): BackstageCredentials<BackstageUserPrincipal>;
export namespace user {
export function header(userEntityRef?: string): string;
// (undocumented)
export function invalidHeader(): string;
// (undocumented)
export function invalidToken(): string;
export function token(userEntityRef?: string): string;
export type TokenPayload = {
userEntityRef?: string;
};
}
}
@@ -159,12 +167,19 @@ export namespace mockServices {
partialImpl?: Partial<DiscoveryService> | undefined,
) => ServiceMock<DiscoveryService>;
}
// (undocumented)
export function httpAuth(options?: { pluginId?: string }): HttpAuthService;
export function httpAuth(options?: {
pluginId?: string;
defaultCredentials?: BackstageCredentials;
}): HttpAuthService;
// (undocumented)
export namespace httpAuth {
const // (undocumented)
factory: () => ServiceFactory<HttpAuthService, 'plugin'>;
const factory: (
options?:
| {
defaultCredentials?: BackstageCredentials | undefined;
}
| undefined,
) => ServiceFactory<HttpAuthService, 'plugin'>;
const // (undocumented)
mock: (
partialImpl?: Partial<HttpAuthService> | undefined,
@@ -72,23 +72,32 @@ describe('MockAuthService', () => {
auth.authenticate(mockCredentials.service.token()),
).resolves.toEqual(mockCredentials.service(DEFAULT_MOCK_SERVICE_SUBJECT));
await expect(
auth.authenticate(mockCredentials.service.token()),
).resolves.toEqual(mockCredentials.service());
await expect(
auth.authenticate(
mockCredentials.service.token({ subject: 'plugin:catalog' }),
mockCredentials.service.token({
onBehalfOf: mockCredentials.service('plugin:catalog'),
targetPluginId: 'test',
}),
),
).resolves.toEqual(mockCredentials.service('plugin:catalog'));
await expect(
auth.authenticate(
mockCredentials.service.token({
onBehalfOf: await auth.getOwnServiceCredentials(),
targetPluginId: 'test',
}),
),
).resolves.toEqual(mockCredentials.service());
).resolves.toEqual(mockCredentials.service('plugin:test'));
await expect(
auth.authenticate(
mockCredentials.service.token({
onBehalfOf: await auth.getOwnServiceCredentials(),
targetPluginId: 'other',
}),
),
@@ -135,7 +144,12 @@ describe('MockAuthService', () => {
onBehalfOf: mockCredentials.user(),
targetPluginId: 'test',
}),
).resolves.toEqual({ token: mockCredentials.user.token() });
).resolves.toEqual({
token: mockCredentials.service.token({
onBehalfOf: mockCredentials.user(),
targetPluginId: 'test',
}),
});
await expect(
auth.getPluginRequestToken({
@@ -143,7 +157,10 @@ describe('MockAuthService', () => {
targetPluginId: 'test',
}),
).resolves.toEqual({
token: mockCredentials.user.token('user:default/other'),
token: mockCredentials.service.token({
onBehalfOf: mockCredentials.user('user:default/other'),
targetPluginId: 'test',
}),
});
await expect(
@@ -153,6 +170,7 @@ describe('MockAuthService', () => {
}),
).resolves.toEqual({
token: mockCredentials.service.token({
onBehalfOf: mockCredentials.service(),
targetPluginId: 'test',
}),
});
@@ -164,7 +182,7 @@ describe('MockAuthService', () => {
}),
).resolves.toEqual({
token: mockCredentials.service.token({
subject: 'external:other',
onBehalfOf: mockCredentials.service('external:other'),
targetPluginId: 'test',
}),
});
@@ -176,7 +194,7 @@ describe('MockAuthService', () => {
}),
).resolves.toEqual({
token: mockCredentials.service.token({
subject: 'plugin:test',
onBehalfOf: await mockCredentials.service('plugin:test'),
targetPluginId: 'other',
}),
});
@@ -29,10 +29,10 @@ import {
MOCK_USER_TOKEN_PREFIX,
MOCK_SERVICE_TOKEN,
MOCK_SERVICE_TOKEN_PREFIX,
DEFAULT_MOCK_USER_ENTITY_REF,
DEFAULT_MOCK_SERVICE_SUBJECT,
MOCK_INVALID_USER_TOKEN,
MOCK_INVALID_SERVICE_TOKEN,
UserTokenPayload,
ServiceTokenPayload,
} from './mockCredentials';
/** @internal */
@@ -55,7 +55,7 @@ export class MockAuthService implements AuthService {
}
if (token.startsWith(MOCK_USER_TOKEN_PREFIX)) {
const { userEntityRef }: mockCredentials.user.TokenPayload = JSON.parse(
const { sub: userEntityRef }: UserTokenPayload = JSON.parse(
token.slice(MOCK_USER_TOKEN_PREFIX.length),
);
@@ -63,16 +63,20 @@ export class MockAuthService implements AuthService {
}
if (token.startsWith(MOCK_SERVICE_TOKEN_PREFIX)) {
const { targetPluginId, subject }: mockCredentials.service.TokenPayload =
JSON.parse(token.slice(MOCK_SERVICE_TOKEN_PREFIX.length));
const { sub, target, obo }: ServiceTokenPayload = JSON.parse(
token.slice(MOCK_SERVICE_TOKEN_PREFIX.length),
);
if (targetPluginId && targetPluginId !== this.pluginId) {
if (target && target !== this.pluginId) {
throw new AuthenticationError(
`Invalid mock token target plugin ID, got '${targetPluginId}' but expected '${this.pluginId}'`,
`Invalid mock token target plugin ID, got '${target}' but expected '${this.pluginId}'`,
);
}
if (obo) {
return mockCredentials.user(obo);
}
return mockCredentials.service(subject);
return mockCredentials.service(sub);
}
throw new AuthenticationError(`Unknown mock token '${token}'`);
@@ -113,28 +117,17 @@ export class MockAuthService implements AuthService {
| BackstageServicePrincipal
| BackstageNonePrincipal;
switch (principal.type) {
case 'user':
if (principal.userEntityRef === DEFAULT_MOCK_USER_ENTITY_REF) {
return { token: mockCredentials.user.token() };
}
return {
token: mockCredentials.user.token(principal.userEntityRef),
};
case 'service':
return {
token: mockCredentials.service.token({
targetPluginId: options.targetPluginId,
subject:
principal.subject === DEFAULT_MOCK_SERVICE_SUBJECT
? undefined
: principal.subject,
}),
};
default:
throw new AuthenticationError(
`Refused to issue service token for credential type '${principal.type}'`,
);
if (principal.type !== 'user' && principal.type !== 'service') {
throw new AuthenticationError(
`Refused to issue service token for credential type '${principal.type}'`,
);
}
return {
token: mockCredentials.service.token({
onBehalfOf: options.onBehalfOf,
targetPluginId: options.targetPluginId,
}),
};
}
}
@@ -81,7 +81,10 @@ describe('MockHttpAuthService', () => {
await expect(
httpAuth.credentials(
makeAuthReq(
mockCredentials.service.header({ subject: 'plugin:other' }),
mockCredentials.service.header({
onBehalfOf: mockCredentials.service('plugin:other'),
targetPluginId: 'test',
}),
),
),
).resolves.toEqual(mockCredentials.service('plugin:other'));
@@ -55,13 +55,13 @@ describe('mockCredentials', () => {
it('creates user tokens and headers', () => {
expect(mockCredentials.user.token()).toBe('mock-user-token');
expect(mockCredentials.user.token('user:default/other')).toBe(
'mock-user-token:{"userEntityRef":"user:default/other"}',
'mock-user-token:{"sub":"user:default/other"}',
);
expect(mockCredentials.user.invalidToken()).toBe('mock-invalid-user-token');
expect(mockCredentials.user.header()).toBe('Bearer mock-user-token');
expect(mockCredentials.user.header('user:default/other')).toBe(
'Bearer mock-user-token:{"userEntityRef":"user:default/other"}',
'Bearer mock-user-token:{"sub":"user:default/other"}',
);
expect(mockCredentials.user.invalidHeader()).toBe(
'Bearer mock-invalid-user-token',
@@ -70,60 +70,38 @@ describe('mockCredentials', () => {
it('creates service tokens and headers', () => {
expect(mockCredentials.service.token()).toBe('mock-service-token');
expect(mockCredentials.service.token({ subject: 'external:other' })).toBe(
'mock-service-token:{"subject":"external:other"}',
);
expect(
mockCredentials.service.token({
onBehalfOf: mockCredentials.service('external:other'),
targetPluginId: 'other',
}),
).toBe('mock-service-token:{"targetPluginId":"other"}');
).toBe('mock-service-token:{"sub":"external:other","target":"other"}');
expect(
mockCredentials.service.token({
subject: 'external:other',
onBehalfOf: mockCredentials.user('user:default/other'),
targetPluginId: 'other',
}),
).toBe(
'mock-service-token:{"subject":"external:other","targetPluginId":"other"}',
);
// Object keys are reordered, ordering in the token should be stable
expect(
mockCredentials.service.token({
targetPluginId: 'other',
subject: 'external:other',
}),
).toBe(
'mock-service-token:{"subject":"external:other","targetPluginId":"other"}',
);
).toBe('mock-service-token:{"obo":"user:default/other","target":"other"}');
expect(mockCredentials.service.invalidToken()).toBe(
'mock-invalid-service-token',
);
expect(mockCredentials.service.header()).toBe('Bearer mock-service-token');
expect(mockCredentials.service.header({ subject: 'external:other' })).toBe(
'Bearer mock-service-token:{"subject":"external:other"}',
);
expect(
mockCredentials.service.header({
targetPluginId: 'other',
}),
).toBe('Bearer mock-service-token:{"targetPluginId":"other"}');
expect(
mockCredentials.service.header({
subject: 'external:other',
onBehalfOf: mockCredentials.service('external:other'),
targetPluginId: 'other',
}),
).toBe(
'Bearer mock-service-token:{"subject":"external:other","targetPluginId":"other"}',
'Bearer mock-service-token:{"sub":"external:other","target":"other"}',
);
// Object keys are reordered, ordering in the token should be stable
expect(
mockCredentials.service.header({
onBehalfOf: mockCredentials.user('user:default/other'),
targetPluginId: 'other',
subject: 'external:other',
}),
).toBe(
'Bearer mock-service-token:{"subject":"external:other","targetPluginId":"other"}',
'Bearer mock-service-token:{"obo":"user:default/other","target":"other"}',
);
expect(mockCredentials.service.invalidHeader()).toBe(
'Bearer mock-invalid-service-token',
@@ -40,6 +40,24 @@ function validateUserEntityRef(ref: string) {
}
}
/**
* The payload that can be encoded into a mock user token.
* @internal
*/
export type UserTokenPayload = {
sub?: string;
};
/**
* The payload that can be encoded into a mock service token.
* @internal
*/
export type ServiceTokenPayload = {
sub?: string; // service subject
obo?: string; // user entity reference
target?: string; // target plugin id
};
/**
* @public
*/
@@ -92,11 +110,6 @@ export namespace mockCredentials {
* Utilities related to user credentials.
*/
export namespace user {
/**
* The payload that can be encoded into a mock user token.
*/
export type TokenPayload = { userEntityRef?: string };
/**
* Creates a mocked user token. If a payload is provided it will be encoded
* into the token and forwarded to the credentials object when authenticated
@@ -105,7 +118,9 @@ export namespace mockCredentials {
export function token(userEntityRef?: string): string {
if (userEntityRef) {
validateUserEntityRef(userEntityRef);
return `${MOCK_USER_TOKEN_PREFIX}${JSON.stringify({ userEntityRef })}`;
return `${MOCK_USER_TOKEN_PREFIX}${JSON.stringify({
sub: userEntityRef,
} satisfies UserTokenPayload)}`;
}
return MOCK_USER_TOKEN;
}
@@ -147,36 +162,47 @@ export namespace mockCredentials {
*/
export namespace service {
/**
* The payload that can be encoded into a mock service token.
* Options for the creation of mock service tokens.
*/
export type TokenPayload = {
subject?: string;
targetPluginId?: string;
export type TokenOptions = {
onBehalfOf: BackstageCredentials;
targetPluginId: string;
};
/**
* Creates a mocked service token. If a payload is provided it will be
* encoded into the token and forwarded to the credentials object when
* authenticated by the mock auth service.
* Creates a mocked service token. The provided options will be encoded into
* the token and forwarded to the credentials object when authenticated by
* the mock auth service.
*/
export function token(payload?: TokenPayload): string {
if (payload) {
const { subject, targetPluginId } = payload; // for fixed ordering
export function token(options?: TokenOptions): string {
if (options) {
const { targetPluginId, onBehalfOf } = options; // for fixed ordering
const oboPrincipal = onBehalfOf?.principal as
| BackstageServicePrincipal
| BackstageUserPrincipal
| BackstageNonePrincipal;
const obo =
oboPrincipal.type === 'user' ? oboPrincipal.userEntityRef : undefined;
const subject =
oboPrincipal.type === 'service' ? oboPrincipal.subject : undefined;
return `${MOCK_SERVICE_TOKEN_PREFIX}${JSON.stringify({
subject,
targetPluginId,
})}`;
sub: subject,
obo,
target: targetPluginId,
} satisfies ServiceTokenPayload)}`;
}
return MOCK_SERVICE_TOKEN;
}
/**
* Returns an authorization header with a mocked service token. If a
* payload is provided it will be encoded into the token and forwarded to
* the credentials object when authenticated by the mock auth service.
* Returns an authorization header with a mocked service token. The provided
* options will be encoded into the token and forwarded to the credentials
* object when authenticated by the mock auth service.
*/
export function header(payload?: TokenPayload): string {
return `Bearer ${token(payload)}`;
export function header(options?: TokenOptions): string {
return `Bearer ${token(options)}`;
}
export function invalidToken(): string {