feat: move out the database saving of the userInfo into the CatalogContext
Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -86,6 +86,7 @@ describe('authPlugin', () => {
|
||||
});
|
||||
|
||||
const refreshRes = await request(server).post('/api/auth/guest/refresh');
|
||||
|
||||
expect(refreshRes.status).toBe(200);
|
||||
expect(refreshRes.body).toMatchObject({
|
||||
backstageIdentity: {
|
||||
@@ -110,7 +111,6 @@ describe('authPlugin', () => {
|
||||
claims: {
|
||||
sub: expectedIdentity.userEntityRef,
|
||||
ent: expectedIdentity.ownershipEntityRefs,
|
||||
exp: expect.any(Number),
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -160,7 +160,6 @@ describe('authPlugin', () => {
|
||||
claims: {
|
||||
sub: expectedIdentity.userEntityRef,
|
||||
ent: expectedIdentity.ownershipEntityRefs,
|
||||
exp: expect.any(Number),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
|
||||
import { Knex } from 'knex';
|
||||
import { UserInfoDatabase } from './UserInfoDatabase';
|
||||
import { AuthDatabase } from './AuthDatabase';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const migrationsDir = resolvePackagePath(
|
||||
'@backstage/plugin-auth-backend',
|
||||
@@ -62,7 +63,6 @@ describe('UserInfoDatabase', () => {
|
||||
claims: {
|
||||
sub: 'user:default/foo',
|
||||
ent: ['group:default/foo-group', 'group:default/bar'],
|
||||
exp: 1234567890,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -71,10 +71,12 @@ describe('UserInfoDatabase', () => {
|
||||
const savedUserInfo = await knex('user_info')
|
||||
.where('user_entity_ref', 'user:default/foo')
|
||||
.first();
|
||||
|
||||
expect(savedUserInfo).toEqual({
|
||||
user_entity_ref: 'user:default/foo',
|
||||
user_info: JSON.stringify(userInfo),
|
||||
exp: expect.anything(),
|
||||
updated_at: expect.any(String),
|
||||
created_at: expect.any(String),
|
||||
});
|
||||
|
||||
userInfo.claims.ent = ['group:default/group1', 'group:default/group2'];
|
||||
@@ -83,10 +85,12 @@ describe('UserInfoDatabase', () => {
|
||||
const updatedUserInfo = await knex('user_info')
|
||||
.where('user_entity_ref', 'user:default/foo')
|
||||
.first();
|
||||
|
||||
expect(updatedUserInfo).toEqual({
|
||||
user_entity_ref: 'user:default/foo',
|
||||
user_info: JSON.stringify(userInfo),
|
||||
exp: expect.anything(),
|
||||
updated_at: expect.any(String),
|
||||
created_at: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,14 +99,13 @@ describe('UserInfoDatabase', () => {
|
||||
claims: {
|
||||
sub: 'user:default/backstage-user',
|
||||
ent: ['group:default/group1', 'group:default/group2'],
|
||||
exp: 1234567890,
|
||||
},
|
||||
};
|
||||
|
||||
await knex('user_info').insert({
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info: JSON.stringify(userInfo),
|
||||
exp: knex.fn.now(),
|
||||
updated_at: DateTime.now().toSQL({ includeOffset: false }),
|
||||
});
|
||||
|
||||
const savedUserInfo = await dbHandler.getUserInfo(
|
||||
|
||||
@@ -17,19 +17,19 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
import { BackstageTokenPayload } from '../identity/TokenFactory';
|
||||
import { AuthDatabase } from './AuthDatabase';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
|
||||
const TABLE = 'user_info';
|
||||
|
||||
type Row = {
|
||||
user_entity_ref: string;
|
||||
user_info: string;
|
||||
exp: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type UserInfo = {
|
||||
claims: Omit<BackstageTokenPayload, 'aud' | 'iat' | 'iss' | 'uip'>;
|
||||
claims: JsonObject;
|
||||
};
|
||||
|
||||
export class UserInfoDatabase {
|
||||
@@ -40,9 +40,7 @@ export class UserInfoDatabase {
|
||||
.insert({
|
||||
user_entity_ref: userInfo.claims.sub as string,
|
||||
user_info: JSON.stringify(userInfo),
|
||||
exp: DateTime.fromSeconds(userInfo.claims.exp as number, {
|
||||
zone: 'utc',
|
||||
}).toSQL({ includeOffset: false }),
|
||||
updated_at: DateTime.now().toSQL({ includeOffset: false }),
|
||||
})
|
||||
.onConflict('user_entity_ref')
|
||||
.merge();
|
||||
|
||||
@@ -18,8 +18,6 @@ import { createLocalJWKSet, jwtVerify } from 'jose';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { StaticKeyStore } from './StaticKeyStore';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { UserInfoDatabase } from '../database/UserInfoDatabase';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
const logger = mockServices.logger.mock();
|
||||
const entityRef = stringifyEntityRef({
|
||||
@@ -29,10 +27,6 @@ const entityRef = stringifyEntityRef({
|
||||
});
|
||||
|
||||
describe('StaticTokenIssuer', () => {
|
||||
const mockUserInfo = {
|
||||
addUserInfo: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as UserInfoDatabase;
|
||||
|
||||
const staticKeyStore = {
|
||||
listKeys: () => {
|
||||
return Promise.resolve({
|
||||
@@ -85,7 +79,6 @@ describe('StaticTokenIssuer', () => {
|
||||
logger,
|
||||
issuer: 'my-issuer',
|
||||
sessionExpirationSeconds: keyDurationSeconds,
|
||||
userInfo: mockUserInfo,
|
||||
},
|
||||
staticKeyStore as unknown as StaticKeyStore,
|
||||
);
|
||||
@@ -119,9 +112,6 @@ describe('StaticTokenIssuer', () => {
|
||||
expect(verifyResult.payload.exp).toBe(
|
||||
verifyResult.payload.iat! + keyDurationSeconds,
|
||||
);
|
||||
expect(mockUserInfo.addUserInfo).toHaveBeenCalledWith({
|
||||
claims: omit(verifyResult.payload, ['aud', 'iat', 'iss', 'uip']),
|
||||
});
|
||||
});
|
||||
|
||||
it('should issue valid tokens with omitted claims', async () => {
|
||||
@@ -131,7 +121,6 @@ describe('StaticTokenIssuer', () => {
|
||||
logger,
|
||||
issuer: 'my-issuer',
|
||||
sessionExpirationSeconds: keyDurationSeconds,
|
||||
userInfo: mockUserInfo,
|
||||
omitClaimsFromToken: ['ent'],
|
||||
},
|
||||
staticKeyStore as unknown as StaticKeyStore,
|
||||
@@ -165,11 +154,5 @@ describe('StaticTokenIssuer', () => {
|
||||
expect(verifyResult.payload.exp).toBe(
|
||||
verifyResult.payload.iat! + keyDurationSeconds,
|
||||
);
|
||||
expect(mockUserInfo.addUserInfo).toHaveBeenCalledWith({
|
||||
claims: {
|
||||
...omit(verifyResult.payload, ['aud', 'iat', 'iss', 'uip']),
|
||||
ent: [entityRef],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
BackstageSignInResult,
|
||||
TokenParams,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { UserInfoDatabase } from '../database/UserInfoDatabase';
|
||||
import { issueUserToken } from './issueUserToken';
|
||||
|
||||
export type Config = {
|
||||
@@ -42,7 +41,6 @@ export type Options = {
|
||||
* A list of claims to omit from issued tokens and only store in the user info database
|
||||
*/
|
||||
omitClaimsFromToken?: string[];
|
||||
userInfo: UserInfoDatabase;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -55,7 +53,6 @@ export class StaticTokenIssuer implements TokenIssuer {
|
||||
private readonly keyStore: StaticKeyStore;
|
||||
private readonly sessionExpirationSeconds: number;
|
||||
private readonly omitClaimsFromToken?: string[];
|
||||
private readonly userInfo: UserInfoDatabase;
|
||||
|
||||
public constructor(options: Options, keyStore: StaticKeyStore) {
|
||||
this.issuer = options.issuer;
|
||||
@@ -63,10 +60,11 @@ export class StaticTokenIssuer implements TokenIssuer {
|
||||
this.sessionExpirationSeconds = options.sessionExpirationSeconds;
|
||||
this.keyStore = keyStore;
|
||||
this.omitClaimsFromToken = options.omitClaimsFromToken;
|
||||
this.userInfo = options.userInfo;
|
||||
}
|
||||
|
||||
public async issueToken(params: TokenParams): Promise<BackstageSignInResult> {
|
||||
public async issueToken(
|
||||
params: TokenParams & { claims: { ent: string[] } },
|
||||
): Promise<BackstageSignInResult> {
|
||||
const key = await this.getSigningKey();
|
||||
|
||||
return issueUserToken({
|
||||
@@ -76,7 +74,6 @@ export class StaticTokenIssuer implements TokenIssuer {
|
||||
logger: this.logger,
|
||||
omitClaimsFromToken: this.omitClaimsFromToken,
|
||||
params,
|
||||
userInfo: this.userInfo,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,8 @@ import {
|
||||
decodeProtectedHeader,
|
||||
jwtVerify,
|
||||
} from 'jose';
|
||||
import { omit } from 'lodash';
|
||||
import { MemoryKeyStore } from './MemoryKeyStore';
|
||||
import { TokenFactory } from './TokenFactory';
|
||||
import { UserInfoDatabase } from '../database/UserInfoDatabase';
|
||||
import { tokenTypes } from '@backstage/plugin-auth-node';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
|
||||
@@ -45,10 +43,6 @@ const entityRef = stringifyEntityRef({
|
||||
});
|
||||
|
||||
describe('TokenFactory', () => {
|
||||
const mockUserInfoDatabaseHandler = {
|
||||
addUserInfo: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as UserInfoDatabase;
|
||||
|
||||
it('should issue valid tokens signed by a listed key', async () => {
|
||||
const keyDurationSeconds = 5;
|
||||
const factory = new TokenFactory({
|
||||
@@ -56,7 +50,6 @@ describe('TokenFactory', () => {
|
||||
keyStore: new MemoryKeyStore(),
|
||||
keyDurationSeconds,
|
||||
logger,
|
||||
userInfo: mockUserInfoDatabaseHandler,
|
||||
});
|
||||
|
||||
await expect(factory.listPublicKeys()).resolves.toEqual({ keys: [] });
|
||||
@@ -93,10 +86,6 @@ describe('TokenFactory', () => {
|
||||
verifyResult.payload.iat! + keyDurationSeconds,
|
||||
);
|
||||
|
||||
expect(mockUserInfoDatabaseHandler.addUserInfo).toHaveBeenCalledWith({
|
||||
claims: omit(verifyResult.payload, ['aud', 'iat', 'iss', 'uip']),
|
||||
});
|
||||
|
||||
// Emulate the reconstruction of a limited user token
|
||||
const limitedUserToken = [
|
||||
base64url.encode(
|
||||
@@ -139,14 +128,13 @@ describe('TokenFactory', () => {
|
||||
keyStore: new MemoryKeyStore(),
|
||||
keyDurationSeconds: 5,
|
||||
logger,
|
||||
userInfo: mockUserInfoDatabaseHandler,
|
||||
});
|
||||
|
||||
const { token: token1 } = await factory.issueToken({
|
||||
claims: { sub: entityRef },
|
||||
claims: { sub: entityRef, ent: [entityRef] },
|
||||
});
|
||||
const { token: token2 } = await factory.issueToken({
|
||||
claims: { sub: entityRef },
|
||||
claims: { sub: entityRef, ent: [entityRef] },
|
||||
});
|
||||
expect(jwtKid(token1)).toBe(jwtKid(token2));
|
||||
|
||||
@@ -165,7 +153,7 @@ describe('TokenFactory', () => {
|
||||
});
|
||||
|
||||
const { token: token3 } = await factory.issueToken({
|
||||
claims: { sub: entityRef },
|
||||
claims: { sub: entityRef, ent: [entityRef] },
|
||||
});
|
||||
expect(jwtKid(token3)).not.toBe(jwtKid(token2));
|
||||
|
||||
@@ -185,12 +173,11 @@ describe('TokenFactory', () => {
|
||||
keyStore: new MemoryKeyStore(),
|
||||
keyDurationSeconds,
|
||||
logger,
|
||||
userInfo: mockUserInfoDatabaseHandler,
|
||||
});
|
||||
|
||||
await expect(() => {
|
||||
return factory.issueToken({
|
||||
claims: { sub: 'UserId' },
|
||||
claims: { sub: 'UserId', ent: [entityRef] },
|
||||
});
|
||||
}).rejects.toThrow();
|
||||
});
|
||||
@@ -203,12 +190,11 @@ describe('TokenFactory', () => {
|
||||
keyDurationSeconds,
|
||||
logger,
|
||||
algorithm: '',
|
||||
userInfo: mockUserInfoDatabaseHandler,
|
||||
});
|
||||
|
||||
await expect(() => {
|
||||
return factory.issueToken({
|
||||
claims: { sub: 'UserId' },
|
||||
claims: { sub: 'UserId', ent: [entityRef] },
|
||||
});
|
||||
}).rejects.toThrow();
|
||||
});
|
||||
@@ -219,7 +205,6 @@ describe('TokenFactory', () => {
|
||||
keyStore: new MemoryKeyStore(),
|
||||
keyDurationSeconds: 5,
|
||||
logger,
|
||||
userInfo: mockUserInfoDatabaseHandler,
|
||||
});
|
||||
|
||||
await expect(() => {
|
||||
@@ -238,7 +223,6 @@ describe('TokenFactory', () => {
|
||||
keyStore: new MemoryKeyStore(),
|
||||
keyDurationSeconds,
|
||||
logger,
|
||||
userInfo: mockUserInfoDatabaseHandler,
|
||||
});
|
||||
|
||||
const { token } = await factory.issueToken({
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { AnyJWK, KeyStore, TokenIssuer } from './types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { UserInfoDatabase } from '../database/UserInfoDatabase';
|
||||
import { issueUserToken } from './issueUserToken';
|
||||
|
||||
/**
|
||||
@@ -92,7 +91,6 @@ type Options = {
|
||||
* A list of claims to omit from issued tokens and only store in the user info database
|
||||
*/
|
||||
omitClaimsFromToken?: string[];
|
||||
userInfo: UserInfoDatabase;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -116,7 +114,6 @@ export class TokenFactory implements TokenIssuer {
|
||||
private readonly keyDurationSeconds: number;
|
||||
private readonly algorithm: string;
|
||||
private readonly omitClaimsFromToken?: string[];
|
||||
private readonly userInfo: UserInfoDatabase;
|
||||
|
||||
private keyExpiry?: Date;
|
||||
private privateKeyPromise?: Promise<JWK>;
|
||||
@@ -128,10 +125,11 @@ export class TokenFactory implements TokenIssuer {
|
||||
this.keyDurationSeconds = options.keyDurationSeconds;
|
||||
this.algorithm = options.algorithm ?? 'ES256';
|
||||
this.omitClaimsFromToken = options.omitClaimsFromToken;
|
||||
this.userInfo = options.userInfo;
|
||||
}
|
||||
|
||||
async issueToken(params: TokenParams): Promise<BackstageSignInResult> {
|
||||
async issueToken(
|
||||
params: TokenParams & { claims: { ent: string[] } },
|
||||
): Promise<BackstageSignInResult> {
|
||||
const key = await this.getKey();
|
||||
|
||||
return issueUserToken({
|
||||
@@ -141,7 +139,6 @@ export class TokenFactory implements TokenIssuer {
|
||||
logger: this.logger,
|
||||
omitClaimsFromToken: this.omitClaimsFromToken,
|
||||
params,
|
||||
userInfo: this.userInfo,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
tokenTypes,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { omit } from 'lodash';
|
||||
import { UserInfoDatabase } from '../database/UserInfoDatabase';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { GeneralSign, importJWK, JWK, KeyLike, SignJWT } from 'jose';
|
||||
import { BackstageTokenPayload } from './TokenFactory';
|
||||
@@ -37,17 +36,15 @@ export async function issueUserToken({
|
||||
logger,
|
||||
omitClaimsFromToken,
|
||||
params,
|
||||
userInfo,
|
||||
}: {
|
||||
issuer: string;
|
||||
key: JWK;
|
||||
keyDurationSeconds: number;
|
||||
logger: LoggerService;
|
||||
omitClaimsFromToken?: string[];
|
||||
params: TokenParams;
|
||||
userInfo: UserInfoDatabase;
|
||||
params: TokenParams & { claims: { ent: string[] } };
|
||||
}): Promise<BackstageSignInResult> {
|
||||
const { sub, ent = [sub], ...additionalClaims } = params.claims;
|
||||
const { sub, ent, ...additionalClaims } = params.claims;
|
||||
const aud = tokenTypes.user.audClaim;
|
||||
const iat = Math.floor(Date.now() / MS_IN_S);
|
||||
const exp = iat + keyDurationSeconds;
|
||||
@@ -109,12 +106,6 @@ export async function issueUserToken({
|
||||
);
|
||||
}
|
||||
|
||||
// Store the user info in the database upon successful token
|
||||
// issuance so that it can be retrieved later by limited user tokens
|
||||
await userInfo.addUserInfo({
|
||||
claims: omit(claims, ['aud', 'iat', 'iss', 'uip']),
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
identity: {
|
||||
|
||||
@@ -19,6 +19,7 @@ import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { TokenIssuer } from '../../identity/types';
|
||||
import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { UserInfoDatabase } from '../../database/UserInfoDatabase';
|
||||
|
||||
describe('CatalogAuthResolverContext', () => {
|
||||
beforeEach(() => {
|
||||
@@ -28,6 +29,16 @@ describe('CatalogAuthResolverContext', () => {
|
||||
const catalog = catalogServiceMock();
|
||||
jest.spyOn(catalog, 'getEntities');
|
||||
|
||||
const mockUserInfo = {
|
||||
addUserInfo: jest.fn().mockResolvedValue(undefined),
|
||||
getUserInfo: jest.fn().mockResolvedValue({
|
||||
claims: {
|
||||
sub: 'user:default/user',
|
||||
ent: ['user:default/user'],
|
||||
},
|
||||
}),
|
||||
} as unknown as jest.Mocked<UserInfoDatabase>;
|
||||
|
||||
it('adds kind to filter when missing', async () => {
|
||||
const auth = mockServices.auth();
|
||||
const context = CatalogAuthResolverContext.create({
|
||||
@@ -35,6 +46,7 @@ describe('CatalogAuthResolverContext', () => {
|
||||
catalog,
|
||||
tokenIssuer: {} as TokenIssuer,
|
||||
auth,
|
||||
userInfo: mockUserInfo,
|
||||
});
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
TokenParams,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { CatalogIdentityClient } from '../catalog/CatalogIdentityClient';
|
||||
import { UserInfoDatabase } from '../../database/UserInfoDatabase';
|
||||
|
||||
function getDefaultOwnershipEntityRefs(entity: Entity) {
|
||||
const membershipRefs =
|
||||
@@ -51,6 +52,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
|
||||
tokenIssuer: TokenIssuer;
|
||||
auth: AuthService;
|
||||
ownershipResolver?: AuthOwnershipResolver;
|
||||
userInfo: UserInfoDatabase;
|
||||
}): CatalogAuthResolverContext {
|
||||
const catalogIdentityClient = new CatalogIdentityClient({
|
||||
catalog: options.catalog,
|
||||
@@ -63,6 +65,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
|
||||
catalogIdentityClient,
|
||||
options.catalog,
|
||||
options.auth,
|
||||
options.userInfo,
|
||||
options.ownershipResolver,
|
||||
);
|
||||
}
|
||||
@@ -73,11 +76,29 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
|
||||
public readonly catalogIdentityClient: CatalogIdentityClient,
|
||||
private readonly catalog: CatalogService,
|
||||
private readonly auth: AuthService,
|
||||
private readonly userInfo: UserInfoDatabase,
|
||||
private readonly ownershipResolver?: AuthOwnershipResolver,
|
||||
) {}
|
||||
|
||||
async issueToken(params: TokenParams) {
|
||||
return await this.tokenIssuer.issueToken(params);
|
||||
// todo(blam): ideally, we would update the token issuer to require the ent claim
|
||||
// instead of doing the destructuring in two places. But that would be a breaking change.
|
||||
const { sub, ent = [sub], ...additionalClaims } = params.claims;
|
||||
const claims = {
|
||||
sub,
|
||||
ent,
|
||||
...additionalClaims,
|
||||
};
|
||||
|
||||
// Store the user info in the database upon successful token
|
||||
// issuance so that it can be retrieved later by limited user tokens
|
||||
await this.userInfo.addUserInfo({
|
||||
claims,
|
||||
});
|
||||
|
||||
return await this.tokenIssuer.issueToken({
|
||||
claims,
|
||||
});
|
||||
}
|
||||
|
||||
async findCatalogUser(query: AuthResolverCatalogUserQuery) {
|
||||
@@ -160,7 +181,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
|
||||
entity,
|
||||
);
|
||||
|
||||
return await this.tokenIssuer.issueToken({
|
||||
return await this.issueToken({
|
||||
claims: {
|
||||
sub: stringifyEntityRef(entity),
|
||||
ent: ownershipEntityRefs,
|
||||
@@ -180,7 +201,7 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
|
||||
}),
|
||||
);
|
||||
|
||||
return await this.tokenIssuer.issueToken({
|
||||
return await this.issueToken({
|
||||
claims: {
|
||||
sub: userEntityRef,
|
||||
ent: [userEntityRef],
|
||||
|
||||
@@ -107,4 +107,37 @@ describe('migrations', () => {
|
||||
await knex.destroy();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'20250707164600_user_created_at.js, %p',
|
||||
async databaseId => {
|
||||
const knex = await databases.init(databaseId);
|
||||
|
||||
await migrateUntilBefore(knex, '20250707164600_user_created_at.js');
|
||||
|
||||
const user_info = JSON.stringify({
|
||||
claims: {
|
||||
ent: ['group:default/group1', 'group:default/group2'],
|
||||
},
|
||||
});
|
||||
|
||||
await knex
|
||||
.insert({
|
||||
user_entity_ref: 'user:default/backstage-user',
|
||||
user_info,
|
||||
exp: knex.fn.now(),
|
||||
})
|
||||
.into('user_info');
|
||||
|
||||
const { exp } = await knex('user_info').first();
|
||||
|
||||
await migrateUpOnce(knex);
|
||||
|
||||
const { created_at, updated_at } = await knex('user_info').first();
|
||||
|
||||
expect(updated_at).toBe(exp);
|
||||
|
||||
expect(created_at).toBeDefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ import Router from 'express-promise-router';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { CatalogAuthResolverContext } from '../lib/resolvers/CatalogAuthResolverContext';
|
||||
import { TokenIssuer } from '../identity/types';
|
||||
import { UserInfoDatabase } from '../database/UserInfoDatabase';
|
||||
|
||||
export type ProviderFactories = { [s: string]: AuthProviderFactory };
|
||||
|
||||
@@ -40,6 +41,7 @@ export function bindProviderRouters(
|
||||
logger: LoggerService;
|
||||
auth: AuthService;
|
||||
tokenIssuer: TokenIssuer;
|
||||
userInfo: UserInfoDatabase;
|
||||
ownershipResolver?: AuthOwnershipResolver;
|
||||
catalog: CatalogService;
|
||||
},
|
||||
@@ -54,6 +56,7 @@ export function bindProviderRouters(
|
||||
tokenIssuer,
|
||||
catalog,
|
||||
ownershipResolver,
|
||||
userInfo,
|
||||
} = options;
|
||||
|
||||
const providersConfig = config.getOptionalConfig('auth.providers');
|
||||
@@ -82,6 +85,7 @@ export function bindProviderRouters(
|
||||
tokenIssuer,
|
||||
auth,
|
||||
ownershipResolver,
|
||||
userInfo,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -94,7 +94,6 @@ export async function createRouter(
|
||||
logger: logger.child({ component: 'token-factory' }),
|
||||
issuer: authUrl,
|
||||
sessionExpirationSeconds: backstageTokenExpiration,
|
||||
userInfo,
|
||||
omitClaimsFromToken,
|
||||
},
|
||||
keyStore as StaticKeyStore,
|
||||
@@ -108,7 +107,6 @@ export async function createRouter(
|
||||
algorithm:
|
||||
tokenFactoryAlgorithm ??
|
||||
config.getOptionalString('auth.identityTokenAlgorithm'),
|
||||
userInfo,
|
||||
omitClaimsFromToken,
|
||||
});
|
||||
}
|
||||
@@ -146,6 +144,7 @@ export async function createRouter(
|
||||
tokenIssuer,
|
||||
...options,
|
||||
auth: options.auth,
|
||||
userInfo,
|
||||
});
|
||||
|
||||
const oidcRouter = OidcRouter.create({
|
||||
|
||||
Reference in New Issue
Block a user