Skip to content

Commit

Permalink
API tokens scoped to deleted projects shouldn't give wildcard access (#…
Browse files Browse the repository at this point in the history
…7499)

If you have SDK tokens scoped to projects that are deleted, you should
not get access to any flags with those.

---------

Co-authored-by: David Leek <[email protected]>
  • Loading branch information
Tymek and daveleek authored Jul 8, 2024
1 parent e7d0748 commit 225d8a9
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 64 deletions.
1 change: 1 addition & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ exports[`should create default config 1`] = `
"flagResolver": FlagResolver {
"experiments": {
"adminTokenKillSwitch": false,
"allowOrphanedWildcardTokens": false,
"anonymiseEventLog": false,
"anonymizeProjectOwners": false,
"automatedActions": false,
Expand Down
106 changes: 72 additions & 34 deletions src/lib/db/api-token-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import type { Logger, LogProvider } from '../logger';
import NotFoundError from '../error/notfound-error';
import type { IApiTokenStore } from '../types/stores/api-token-store';
import {
type ApiTokenType,
ApiTokenType,
type IApiToken,
type IApiTokenCreate,
isAllProjects,
} from '../types/models/api-token';
import { ALL_PROJECTS } from '../util/constants';
import type { Db } from './db';
import { inTransaction } from './transaction';
import type { IFlagResolver } from '../types';

const TABLE = 'api_tokens';
const API_LINK_TABLE = 'api_token_project';
Expand All @@ -35,33 +36,44 @@ interface ITokenRow extends ITokenInsert {
project: string;
}

const tokenRowReducer = (acc, tokenRow) => {
const { project, ...token } = tokenRow;
if (!acc[tokenRow.secret]) {
acc[tokenRow.secret] = {
secret: token.secret,
tokenName: token.token_name ? token.token_name : token.username,
type: token.type.toLowerCase(),
project: ALL,
projects: [ALL],
environment: token.environment ? token.environment : ALL,
expiresAt: token.expires_at,
createdAt: token.created_at,
alias: token.alias,
seenAt: token.seen_at,
username: token.token_name ? token.token_name : token.username,
};
}
const currentToken = acc[tokenRow.secret];
if (tokenRow.project) {
if (isAllProjects(currentToken.projects)) {
currentToken.projects = [];
const createTokenRowReducer =
(allowOrphanedWildcardTokens: boolean) => (acc, tokenRow) => {
const { project, ...token } = tokenRow;
if (!acc[tokenRow.secret]) {
if (
!allowOrphanedWildcardTokens &&
!tokenRow.project &&
!tokenRow.secret.startsWith('*:') &&
(tokenRow.type === ApiTokenType.CLIENT ||
tokenRow.type === ApiTokenType.FRONTEND)
) {
return acc;
}

acc[tokenRow.secret] = {
secret: token.secret,
tokenName: token.token_name ? token.token_name : token.username,
type: token.type.toLowerCase(),
project: ALL,
projects: [ALL],
environment: token.environment ? token.environment : ALL,
expiresAt: token.expires_at,
createdAt: token.created_at,
alias: token.alias,
seenAt: token.seen_at,
username: token.token_name ? token.token_name : token.username,
};
}
currentToken.projects.push(tokenRow.project);
currentToken.project = currentToken.projects.join(',');
}
return acc;
};
const currentToken = acc[tokenRow.secret];
if (tokenRow.project) {
if (isAllProjects(currentToken.projects)) {
currentToken.projects = [];
}
currentToken.projects.push(tokenRow.project);
currentToken.project = currentToken.projects.join(',');
}
return acc;
};

const toRow = (newToken: IApiTokenCreate) => ({
username: newToken.tokenName ?? newToken.username,
Expand All @@ -74,8 +86,14 @@ const toRow = (newToken: IApiTokenCreate) => ({
alias: newToken.alias || null,
});

const toTokens = (rows: any[]): IApiToken[] => {
const tokens = rows.reduce(tokenRowReducer, {});
const toTokens = (
rows: any[],
allowOrphanedWildcardTokens: boolean,
): IApiToken[] => {
const tokens = rows.reduce(
createTokenRowReducer(allowOrphanedWildcardTokens),
{},
);
return Object.values(tokens);
};

Expand All @@ -86,14 +104,22 @@ export class ApiTokenStore implements IApiTokenStore {

private db: Db;

constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
private readonly flagResolver: IFlagResolver;

constructor(
db: Db,
eventBus: EventEmitter,
getLogger: LogProvider,
flagResolver: IFlagResolver,
) {
this.db = db;
this.logger = getLogger('api-tokens.js');
this.timer = (action: string) =>
metricsHelper.wrapTimer(eventBus, DB_TIME, {
store: 'api-tokens',
action,
});
this.flagResolver = flagResolver;
}

async count(): Promise<number> {
Expand All @@ -120,7 +146,10 @@ export class ApiTokenStore implements IApiTokenStore {
const stopTimer = this.timer('getAll');
const rows = await this.makeTokenProjectQuery();
stopTimer();
return toTokens(rows);
const allowOrphanedWildcardTokens = this.flagResolver.isEnabled(
'allowOrphanedWildcardTokens',
);
return toTokens(rows, allowOrphanedWildcardTokens);
}

async getAllActive(): Promise<IApiToken[]> {
Expand All @@ -129,7 +158,10 @@ export class ApiTokenStore implements IApiTokenStore {
.where('expires_at', 'IS', null)
.orWhere('expires_at', '>', 'now()');
stopTimer();
return toTokens(rows);
const allowOrphanedWildcardTokens = this.flagResolver.isEnabled(
'allowOrphanedWildcardTokens',
);
return toTokens(rows, allowOrphanedWildcardTokens);
}

private makeTokenProjectQuery() {
Expand Down Expand Up @@ -200,7 +232,10 @@ export class ApiTokenStore implements IApiTokenStore {
key,
);
stopTimer();
return toTokens(row)[0];
const allowOrphanedWildcardTokens = this.flagResolver.isEnabled(
'allowOrphanedWildcardTokens',
);
return toTokens(row, allowOrphanedWildcardTokens)[0];
}

async delete(secret: string): Promise<void> {
Expand All @@ -217,7 +252,10 @@ export class ApiTokenStore implements IApiTokenStore {
.where({ secret })
.returning('*');
if (rows.length > 0) {
return toTokens(rows)[0];
const allowOrphanedWildcardTokens = this.flagResolver.isEnabled(
'allowOrphanedWildcardTokens',
);
return toTokens(rows, allowOrphanedWildcardTokens)[0];
}
throw new NotFoundError('Could not find api-token.');
}
Expand Down
7 changes: 6 additions & 1 deletion src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ export const createStores = (
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
addonStore: new AddonStore(db, eventBus, getLogger),
accessStore: new AccessStore(db, eventBus, getLogger),
apiTokenStore: new ApiTokenStore(db, eventBus, getLogger),
apiTokenStore: new ApiTokenStore(
db,
eventBus,
getLogger,
config.flagResolver,
),
resetTokenStore: new ResetTokenStore(db, eventBus, getLogger),
sessionStore: new SessionStore(db, eventBus, getLogger),
userFeedbackStore: new UserFeedbackStore(db, eventBus, getLogger),
Expand Down
7 changes: 6 additions & 1 deletion src/lib/features/api-tokens/createApiTokenService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ export const createApiTokenService = (
config: IUnleashConfig,
): ApiTokenService => {
const { eventBus, getLogger } = config;
const apiTokenStore = new ApiTokenStore(db, eventBus, getLogger);
const apiTokenStore = new ApiTokenStore(
db,
eventBus,
getLogger,
config.flagResolver,
);
const environmentStore = new EnvironmentStore(db, eventBus, getLogger);
const eventService = createEventsService(db, config);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
getLogger,
);
const eventStore = new EventStore(db, getLogger);
const apiTokenStore = new ApiTokenStore(db, eventBus, getLogger);
const apiTokenStore = new ApiTokenStore(
db,
eventBus,
getLogger,
flagResolver,
);
const clientMetricsStoreV2 = new ClientMetricsStoreV2(
db,
getLogger,
Expand Down
5 changes: 5 additions & 0 deletions src/lib/types/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type IFlagKey =
| 'flagCreator'
| 'anonymizeProjectOwners'
| 'resourceLimits'
| 'allowOrphanedWildcardTokens'
| 'extendedMetrics';

export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
Expand Down Expand Up @@ -300,6 +301,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_RESOURCE_LIMITS,
false,
),
allowOrphanedWildcardTokens: parseEnvVarBoolean(
process.env.UNLEASH_ORPHANED_TOKENS_KILL_SWITCH,
false,
),
extendedMetrics: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_EXTENDED_METRICS,
false,
Expand Down
Loading

0 comments on commit 225d8a9

Please sign in to comment.