diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 1037a9e542..82577005d6 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -132,6 +132,7 @@ exports[`should create default config 1`] = ` "newStrategyConfigurationFeedback": false, "personalAccessTokensKillSwitch": false, "proPlanAutoCharge": false, + "queryMissingTokens": false, "responseTimeWithAppNameKillSwitch": false, "scimApi": false, "sdkReporting": false, diff --git a/src/lib/middleware/api-token-middleware.ts b/src/lib/middleware/api-token-middleware.ts index 56d8e7c339..4a0d7a8824 100644 --- a/src/lib/middleware/api-token-middleware.ts +++ b/src/lib/middleware/api-token-middleware.ts @@ -2,6 +2,7 @@ import { ApiTokenType } from '../types/models/api-token'; import { IUnleashConfig } from '../types/option'; import { IApiRequest, IAuthRequest } from '../routes/unleash-types'; import { IUnleashServices } from '../server-impl'; +import { IFlagContext } from '../types'; const isClientApi = ({ path }) => { return path && path.indexOf('/api/client') > -1; @@ -26,6 +27,20 @@ const isProxyApi = ({ path }) => { ); }; +const contextFrom = ( + req: IAuthRequest | IApiRequest, +): IFlagContext | undefined => { + // this is what we'd get from edge: + // req_path: '/api/client/features', + // req_user_agent: 'unleash-edge-16.0.4' + return { + reqPath: req.path, + reqUserAgent: req.get ? req.get('User-Agent') ?? '' : '', + reqAppName: + req.headers?.['unleash-appname'] ?? req.query?.appName ?? '', + }; +}; + export const TOKEN_TYPE_ERROR_MESSAGE = 'invalid token: expected a different token type for this endpoint'; @@ -46,7 +61,7 @@ const apiAccessMiddleware = ( return (req, res, next) => next(); } - return (req: IAuthRequest | IApiRequest, res, next) => { + return async (req: IAuthRequest | IApiRequest, res, next) => { if (req.user) { return next(); } @@ -55,7 +70,10 @@ const apiAccessMiddleware = ( const apiToken = req.header('authorization'); if (!apiToken?.startsWith('user:')) { const apiUser = apiToken - ? apiTokenService.getUserForToken(apiToken) + ? await apiTokenService.getUserForToken( + apiToken, + contextFrom(req), + ) : undefined; const { CLIENT, FRONTEND } = ApiTokenType; diff --git a/src/lib/services/api-token-service.test.ts b/src/lib/services/api-token-service.test.ts index a2ee4f51a8..e579065702 100644 --- a/src/lib/services/api-token-service.test.ts +++ b/src/lib/services/api-token-service.test.ts @@ -1,6 +1,6 @@ import { ApiTokenService } from './api-token-service'; import { createTestConfig } from '../../test/config/test-config'; -import { IUnleashConfig, IUser } from '../server-impl'; +import { IUnleashConfig, IUnleashOptions, IUser } from '../server-impl'; import { ApiTokenType, IApiTokenCreate } from '../types/models/api-token'; import FakeApiTokenStore from '../../test/fixtures/fake-api-token-store'; import FakeEnvironmentStore from '../features/project-environments/fake-environment-store'; @@ -95,7 +95,7 @@ test("Shouldn't return frontend token when secret is undefined", async () => { await apiTokenService.createApiTokenWithProjects(token); await apiTokenService.fetchActiveTokens(); - expect(apiTokenService.getUserForToken('')).toEqual(undefined); + expect(await apiTokenService.getUserForToken('')).toEqual(undefined); }); test('Api token operations should all have events attached', async () => { @@ -185,8 +185,62 @@ test('getUserForToken should get a user with admin token user id and token name' ADMIN_TOKEN_USER as IUser, ); - const user = tokenService.getUserForToken(token.secret); + const user = await tokenService.getUserForToken(token.secret); expect(user).toBeDefined(); expect(user!.username).toBe(token.tokenName); expect(user!.internalAdminTokenUserId).toBe(ADMIN_TOKEN_USER.id); }); + +describe('When token is added by another instance', () => { + const setup = (options?: IUnleashOptions) => { + const token: IApiTokenCreate = { + environment: 'default', + projects: ['*'], + secret: '*:*:some-random-string', + type: ApiTokenType.CLIENT, + tokenName: 'new-token-by-another-instance', + expiresAt: undefined, + }; + + const config: IUnleashConfig = createTestConfig(options); + const apiTokenStore = new FakeApiTokenStore(); + const environmentStore = new FakeEnvironmentStore(); + + const apiTokenService = new ApiTokenService( + { apiTokenStore, environmentStore }, + config, + createFakeEventsService(config), + ); + return { + apiTokenService, + apiTokenStore, + token, + }; + }; + test('should not return the token when query db flag is disabled', async () => { + const { apiTokenService, apiTokenStore, token } = setup(); + + // simulate this token being inserted by another instance (apiTokenService does not know about it) + apiTokenStore.insert(token); + + const found = await apiTokenService.getUserForToken(token.secret); + expect(found).toBeUndefined(); + }); + + test('should return the token when query db flag is enabled', async () => { + const { apiTokenService, apiTokenStore, token } = setup({ + experimental: { + flags: { + queryMissingTokens: true, + }, + }, + }); + + // simulate this token being inserted by another instance (apiTokenService does not know about it) + apiTokenStore.insert(token); + + const found = await apiTokenService.getUserForToken(token.secret); + expect(found).toBeDefined(); + expect(found?.username).toBe(token.tokenName); + }); +}); diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 10e198d659..fe9c09a7fc 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -24,6 +24,7 @@ import { ApiTokenCreatedEvent, ApiTokenDeletedEvent, ApiTokenUpdatedEvent, + IFlagContext, IFlagResolver, IUser, SYSTEM_USER, @@ -160,7 +161,10 @@ export class ApiTokenService { } } - public getUserForToken(secret: string): IApiUser | undefined { + public async getUserForToken( + secret: string, + flagContext?: IFlagContext, // temporarily added, expected from the middleware + ): Promise { if (!secret) { return undefined; } @@ -181,6 +185,16 @@ export class ApiTokenService { ); } + if ( + !token && + this.flagResolver.isEnabled('queryMissingTokens', flagContext) + ) { + token = await this.store.get(secret); + if (token) { + this.activeTokens.push(token); + } + } + if (token) { this.lastSeenSecrets.add(token.secret); const apiUser: IApiUser = new ApiUser({ diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 1ed54f9df1..2e41b83a45 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -47,6 +47,7 @@ export type IFlagKey = | 'inMemoryScheduledChangeRequests' | 'collectTrafficDataUsage' | 'useMemoizedActiveTokens' + | 'queryMissingTokens' | 'userAccessUIEnabled' | 'disableUpdateMaxRevisionId' | 'disablePublishUnannouncedEvents' @@ -248,6 +249,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_DISABLE_SCHEDULED_CACHES, false, ), + queryMissingTokens: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_QUERY_MISSING_TOKENS, + false, + ), scimApi: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_SCIM_API, false, diff --git a/src/test/e2e/services/api-token-service.e2e.test.ts b/src/test/e2e/services/api-token-service.e2e.test.ts index 41c891876f..54e941ff79 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -200,8 +200,8 @@ test('should return user with multiple projects', async () => { environment: DEFAULT_ENV, }); - const multiProjectUser = apiTokenService.getUserForToken(secret1); - const singleProjectUser = apiTokenService.getUserForToken(secret2); + const multiProjectUser = await apiTokenService.getUserForToken(secret1); + const singleProjectUser = await apiTokenService.getUserForToken(secret2); expect(multiProjectUser!.projects).toStrictEqual([ 'test-project', diff --git a/website/docs/reference/front-end-api.md b/website/docs/reference/front-end-api.md index 7c8bb9e946..a55ee50bb8 100644 --- a/website/docs/reference/front-end-api.md +++ b/website/docs/reference/front-end-api.md @@ -42,7 +42,7 @@ The client needs to point to the correct API endpoint. The front-end API is avai ### API token -You can create appropriate token, with type `FRONTEND` on `/admin/api/create-token` page or with a request to `/api/admin/api-tokens`. See our guide on [how to create API tokens](../how-to/how-to-create-api-tokens.mdx) for more details. +You can create appropriate token, with type `FRONTEND` on `/api/admin/create-token` page or with a request to `/api/admin/api-tokens`. See our guide on [how to create API tokens](../how-to/how-to-create-api-tokens.mdx) for more details. ### Refresh interval for tokens