mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
API tokens scoped to deleted projects shouldn't give wildcard access (#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 <david@getunleash.io>
This commit is contained in:
parent
e7d07486a1
commit
225d8a91f1
@ -76,6 +76,7 @@ exports[`should create default config 1`] = `
|
|||||||
"flagResolver": FlagResolver {
|
"flagResolver": FlagResolver {
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"adminTokenKillSwitch": false,
|
"adminTokenKillSwitch": false,
|
||||||
|
"allowOrphanedWildcardTokens": false,
|
||||||
"anonymiseEventLog": false,
|
"anonymiseEventLog": false,
|
||||||
"anonymizeProjectOwners": false,
|
"anonymizeProjectOwners": false,
|
||||||
"automatedActions": false,
|
"automatedActions": false,
|
||||||
|
@ -5,7 +5,7 @@ import type { Logger, LogProvider } from '../logger';
|
|||||||
import NotFoundError from '../error/notfound-error';
|
import NotFoundError from '../error/notfound-error';
|
||||||
import type { IApiTokenStore } from '../types/stores/api-token-store';
|
import type { IApiTokenStore } from '../types/stores/api-token-store';
|
||||||
import {
|
import {
|
||||||
type ApiTokenType,
|
ApiTokenType,
|
||||||
type IApiToken,
|
type IApiToken,
|
||||||
type IApiTokenCreate,
|
type IApiTokenCreate,
|
||||||
isAllProjects,
|
isAllProjects,
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
import { ALL_PROJECTS } from '../util/constants';
|
import { ALL_PROJECTS } from '../util/constants';
|
||||||
import type { Db } from './db';
|
import type { Db } from './db';
|
||||||
import { inTransaction } from './transaction';
|
import { inTransaction } from './transaction';
|
||||||
|
import type { IFlagResolver } from '../types';
|
||||||
|
|
||||||
const TABLE = 'api_tokens';
|
const TABLE = 'api_tokens';
|
||||||
const API_LINK_TABLE = 'api_token_project';
|
const API_LINK_TABLE = 'api_token_project';
|
||||||
@ -35,33 +36,44 @@ interface ITokenRow extends ITokenInsert {
|
|||||||
project: string;
|
project: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenRowReducer = (acc, tokenRow) => {
|
const createTokenRowReducer =
|
||||||
const { project, ...token } = tokenRow;
|
(allowOrphanedWildcardTokens: boolean) => (acc, tokenRow) => {
|
||||||
if (!acc[tokenRow.secret]) {
|
const { project, ...token } = tokenRow;
|
||||||
acc[tokenRow.secret] = {
|
if (!acc[tokenRow.secret]) {
|
||||||
secret: token.secret,
|
if (
|
||||||
tokenName: token.token_name ? token.token_name : token.username,
|
!allowOrphanedWildcardTokens &&
|
||||||
type: token.type.toLowerCase(),
|
!tokenRow.project &&
|
||||||
project: ALL,
|
!tokenRow.secret.startsWith('*:') &&
|
||||||
projects: [ALL],
|
(tokenRow.type === ApiTokenType.CLIENT ||
|
||||||
environment: token.environment ? token.environment : ALL,
|
tokenRow.type === ApiTokenType.FRONTEND)
|
||||||
expiresAt: token.expires_at,
|
) {
|
||||||
createdAt: token.created_at,
|
return acc;
|
||||||
alias: token.alias,
|
}
|
||||||
seenAt: token.seen_at,
|
|
||||||
username: token.token_name ? token.token_name : token.username,
|
acc[tokenRow.secret] = {
|
||||||
};
|
secret: token.secret,
|
||||||
}
|
tokenName: token.token_name ? token.token_name : token.username,
|
||||||
const currentToken = acc[tokenRow.secret];
|
type: token.type.toLowerCase(),
|
||||||
if (tokenRow.project) {
|
project: ALL,
|
||||||
if (isAllProjects(currentToken.projects)) {
|
projects: [ALL],
|
||||||
currentToken.projects = [];
|
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);
|
const currentToken = acc[tokenRow.secret];
|
||||||
currentToken.project = currentToken.projects.join(',');
|
if (tokenRow.project) {
|
||||||
}
|
if (isAllProjects(currentToken.projects)) {
|
||||||
return acc;
|
currentToken.projects = [];
|
||||||
};
|
}
|
||||||
|
currentToken.projects.push(tokenRow.project);
|
||||||
|
currentToken.project = currentToken.projects.join(',');
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
};
|
||||||
|
|
||||||
const toRow = (newToken: IApiTokenCreate) => ({
|
const toRow = (newToken: IApiTokenCreate) => ({
|
||||||
username: newToken.tokenName ?? newToken.username,
|
username: newToken.tokenName ?? newToken.username,
|
||||||
@ -74,8 +86,14 @@ const toRow = (newToken: IApiTokenCreate) => ({
|
|||||||
alias: newToken.alias || null,
|
alias: newToken.alias || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const toTokens = (rows: any[]): IApiToken[] => {
|
const toTokens = (
|
||||||
const tokens = rows.reduce(tokenRowReducer, {});
|
rows: any[],
|
||||||
|
allowOrphanedWildcardTokens: boolean,
|
||||||
|
): IApiToken[] => {
|
||||||
|
const tokens = rows.reduce(
|
||||||
|
createTokenRowReducer(allowOrphanedWildcardTokens),
|
||||||
|
{},
|
||||||
|
);
|
||||||
return Object.values(tokens);
|
return Object.values(tokens);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,7 +104,14 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
|
|
||||||
private db: Db;
|
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.db = db;
|
||||||
this.logger = getLogger('api-tokens.js');
|
this.logger = getLogger('api-tokens.js');
|
||||||
this.timer = (action: string) =>
|
this.timer = (action: string) =>
|
||||||
@ -94,6 +119,7 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
store: 'api-tokens',
|
store: 'api-tokens',
|
||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
|
this.flagResolver = flagResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
async count(): Promise<number> {
|
async count(): Promise<number> {
|
||||||
@ -120,7 +146,10 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
const stopTimer = this.timer('getAll');
|
const stopTimer = this.timer('getAll');
|
||||||
const rows = await this.makeTokenProjectQuery();
|
const rows = await this.makeTokenProjectQuery();
|
||||||
stopTimer();
|
stopTimer();
|
||||||
return toTokens(rows);
|
const allowOrphanedWildcardTokens = this.flagResolver.isEnabled(
|
||||||
|
'allowOrphanedWildcardTokens',
|
||||||
|
);
|
||||||
|
return toTokens(rows, allowOrphanedWildcardTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllActive(): Promise<IApiToken[]> {
|
async getAllActive(): Promise<IApiToken[]> {
|
||||||
@ -129,7 +158,10 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
.where('expires_at', 'IS', null)
|
.where('expires_at', 'IS', null)
|
||||||
.orWhere('expires_at', '>', 'now()');
|
.orWhere('expires_at', '>', 'now()');
|
||||||
stopTimer();
|
stopTimer();
|
||||||
return toTokens(rows);
|
const allowOrphanedWildcardTokens = this.flagResolver.isEnabled(
|
||||||
|
'allowOrphanedWildcardTokens',
|
||||||
|
);
|
||||||
|
return toTokens(rows, allowOrphanedWildcardTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
private makeTokenProjectQuery() {
|
private makeTokenProjectQuery() {
|
||||||
@ -200,7 +232,10 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
key,
|
key,
|
||||||
);
|
);
|
||||||
stopTimer();
|
stopTimer();
|
||||||
return toTokens(row)[0];
|
const allowOrphanedWildcardTokens = this.flagResolver.isEnabled(
|
||||||
|
'allowOrphanedWildcardTokens',
|
||||||
|
);
|
||||||
|
return toTokens(row, allowOrphanedWildcardTokens)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(secret: string): Promise<void> {
|
async delete(secret: string): Promise<void> {
|
||||||
@ -217,7 +252,10 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
.where({ secret })
|
.where({ secret })
|
||||||
.returning('*');
|
.returning('*');
|
||||||
if (rows.length > 0) {
|
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.');
|
throw new NotFoundError('Could not find api-token.');
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,12 @@ export const createStores = (
|
|||||||
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
|
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
|
||||||
addonStore: new AddonStore(db, eventBus, getLogger),
|
addonStore: new AddonStore(db, eventBus, getLogger),
|
||||||
accessStore: new AccessStore(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),
|
resetTokenStore: new ResetTokenStore(db, eventBus, getLogger),
|
||||||
sessionStore: new SessionStore(db, eventBus, getLogger),
|
sessionStore: new SessionStore(db, eventBus, getLogger),
|
||||||
userFeedbackStore: new UserFeedbackStore(db, eventBus, getLogger),
|
userFeedbackStore: new UserFeedbackStore(db, eventBus, getLogger),
|
||||||
|
@ -15,7 +15,12 @@ export const createApiTokenService = (
|
|||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
): ApiTokenService => {
|
): ApiTokenService => {
|
||||||
const { eventBus, getLogger } = config;
|
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 environmentStore = new EnvironmentStore(db, eventBus, getLogger);
|
||||||
const eventService = createEventsService(db, config);
|
const eventService = createEventsService(db, config);
|
||||||
|
|
||||||
|
@ -78,7 +78,12 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
|
|||||||
getLogger,
|
getLogger,
|
||||||
);
|
);
|
||||||
const eventStore = new EventStore(db, 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(
|
const clientMetricsStoreV2 = new ClientMetricsStoreV2(
|
||||||
db,
|
db,
|
||||||
getLogger,
|
getLogger,
|
||||||
|
@ -63,6 +63,7 @@ export type IFlagKey =
|
|||||||
| 'flagCreator'
|
| 'flagCreator'
|
||||||
| 'anonymizeProjectOwners'
|
| 'anonymizeProjectOwners'
|
||||||
| 'resourceLimits'
|
| 'resourceLimits'
|
||||||
|
| 'allowOrphanedWildcardTokens'
|
||||||
| 'extendedMetrics';
|
| 'extendedMetrics';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
@ -300,6 +301,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_RESOURCE_LIMITS,
|
process.env.UNLEASH_EXPERIMENTAL_RESOURCE_LIMITS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
allowOrphanedWildcardTokens: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_ORPHANED_TOKENS_KILL_SWITCH,
|
||||||
|
false,
|
||||||
|
),
|
||||||
extendedMetrics: parseEnvVarBoolean(
|
extendedMetrics: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_EXTENDED_METRICS,
|
process.env.UNLEASH_EXPERIMENTAL_EXTENDED_METRICS,
|
||||||
false,
|
false,
|
||||||
|
@ -79,7 +79,7 @@ test('editor users should only get client or frontend tokens', async () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'test',
|
username: 'test',
|
||||||
secret: '1234',
|
secret: '*:environment.1234',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ test('editor users should only get client or frontend tokens', async () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'frontend',
|
username: 'frontend',
|
||||||
secret: '12345',
|
secret: '*:environment.12345',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ test('editor users should only get client or frontend tokens', async () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'test',
|
username: 'test',
|
||||||
secret: 'sdfsdf2d',
|
secret: '*:*.sdfsdf2d',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'test',
|
username: 'test',
|
||||||
secret: '1234',
|
secret: '*:environment.1234',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ test('viewer users should not be allowed to fetch tokens', async () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'test',
|
username: 'test',
|
||||||
secret: 'sdfsdf2d',
|
secret: '*:*.sdfsdf2d',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -553,7 +553,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
tokenName: '',
|
tokenName: '',
|
||||||
|
|
||||||
username: 'client',
|
username: 'client',
|
||||||
secret: 'client_secret',
|
secret: '*:environment.client_secret',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -562,7 +562,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
secret: 'sdfsdf2admin_secret',
|
secret: '*:*.sdfsdf2admin_secret',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
@ -570,7 +570,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'frontender',
|
username: 'frontender',
|
||||||
secret: 'sdfsdf2dfrontend_Secret',
|
secret: '*:environment:sdfsdf2dfrontend_Secret',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
@ -637,7 +637,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'client',
|
username: 'client',
|
||||||
secret: 'client_secret_1234',
|
secret: '*:environment.client_secret_1234',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -646,7 +646,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
secret: 'admin_secret_1234',
|
secret: '*:*.admin_secret_1234',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
@ -654,7 +654,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'frontender',
|
username: 'frontender',
|
||||||
secret: 'frontend_secret_1234',
|
secret: '*:environment.frontend_secret_1234',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
@ -699,7 +699,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'client',
|
username: 'client',
|
||||||
secret: 'client_secret_4321',
|
secret: '*:environment.client_secret_4321',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -708,7 +708,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
secret: 'admin_secret_4321',
|
secret: '*:*.admin_secret_4321',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
@ -716,7 +716,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'frontender',
|
username: 'frontender',
|
||||||
secret: 'frontend_secret_4321',
|
secret: '*:environment.frontend_secret_4321',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
@ -760,7 +760,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'client',
|
username: 'client',
|
||||||
secret: 'client_secret_4321',
|
secret: '*:environment.client_secret_4321',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
@ -768,7 +768,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
secret: 'admin_secret_4321',
|
secret: '*:*.admin_secret_4321',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await stores.apiTokenStore.insert({
|
await stores.apiTokenStore.insert({
|
||||||
@ -776,7 +776,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'frontender',
|
username: 'frontender',
|
||||||
secret: 'frontend_secret_4321',
|
secret: '*:environment.frontend_secret_4321',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
@ -848,7 +848,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'cilent',
|
username: 'cilent',
|
||||||
secret: 'update_client_token',
|
secret: '*:environment.update_client_token',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
@ -910,7 +910,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'frontend',
|
username: 'frontend',
|
||||||
secret: 'update_frontend_token',
|
secret: '*:environment.update_frontend_token',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
@ -973,7 +973,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
tokenName: '',
|
tokenName: '',
|
||||||
|
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
secret: 'update_admin_token',
|
secret: '*:*.update_admin_token',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
@ -1038,7 +1038,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'cilent',
|
username: 'cilent',
|
||||||
secret: 'delete_client_token',
|
secret: '*:environment.delete_client_token',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
@ -1100,7 +1100,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'frontend',
|
username: 'frontend',
|
||||||
secret: 'delete_frontend_token',
|
secret: '*:environment.delete_frontend_token',
|
||||||
type: ApiTokenType.FRONTEND,
|
type: ApiTokenType.FRONTEND,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
@ -1161,7 +1161,7 @@ describe('Fine grained API token permissions', () => {
|
|||||||
projects: [],
|
projects: [],
|
||||||
tokenName: '',
|
tokenName: '',
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
secret: 'delete_admin_token',
|
secret: '*:*:delete_admin_token',
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
});
|
});
|
||||||
await request
|
await request
|
||||||
|
@ -122,7 +122,7 @@ test('creates new admin token with expiry', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('update client token with expiry', async () => {
|
test('update client token with expiry', async () => {
|
||||||
const tokenSecret = 'random-secret-update';
|
const tokenSecret = '*:environment.random-secret-update';
|
||||||
|
|
||||||
await db.stores.apiTokenStore.insert({
|
await db.stores.apiTokenStore.insert({
|
||||||
username: 'test',
|
username: 'test',
|
||||||
@ -187,7 +187,7 @@ test('creates a lot of client tokens', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('removes api token', async () => {
|
test('removes api token', async () => {
|
||||||
const tokenSecret = 'random-secret';
|
const tokenSecret = '*:environment.random-secret';
|
||||||
|
|
||||||
await db.stores.apiTokenStore.insert({
|
await db.stores.apiTokenStore.insert({
|
||||||
environment: 'development',
|
environment: 'development',
|
||||||
|
@ -0,0 +1,153 @@
|
|||||||
|
import { type IUnleashTest, setupAppWithAuth } from '../../helpers/test-helper';
|
||||||
|
import dbInit, { type ITestDb } from '../../helpers/database-init';
|
||||||
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
|
import type { ApiTokenService } from '../../../../lib/services/api-token-service';
|
||||||
|
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||||
|
import { DEFAULT_ENV } from '../../../../lib/util/constants';
|
||||||
|
import { TEST_AUDIT_USER } from '../../../../lib/types';
|
||||||
|
import { User } from '../../../../lib/server-impl';
|
||||||
|
|
||||||
|
let app: IUnleashTest;
|
||||||
|
let db: ITestDb;
|
||||||
|
|
||||||
|
let apiTokenService: ApiTokenService;
|
||||||
|
|
||||||
|
const environment = 'testing';
|
||||||
|
const project = 'default';
|
||||||
|
const project2 = 'some';
|
||||||
|
const deletionProject = 'deletion';
|
||||||
|
const deletionTokenName = 'delete';
|
||||||
|
const feature1 = 'f1.token.access';
|
||||||
|
const feature2 = 'f2.token.access';
|
||||||
|
const feature3 = 'f3.p2.token.access';
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('feature_api_api_access_client_deletion', getLogger);
|
||||||
|
app = await setupAppWithAuth(db.stores, {}, db.rawDatabase);
|
||||||
|
apiTokenService = app.services.apiTokenService;
|
||||||
|
|
||||||
|
const { featureToggleServiceV2, environmentService } = app.services;
|
||||||
|
const { environmentStore, projectStore } = db.stores;
|
||||||
|
|
||||||
|
await environmentStore.create({
|
||||||
|
name: environment,
|
||||||
|
type: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectStore.create({
|
||||||
|
id: project2,
|
||||||
|
name: 'Test Project 2',
|
||||||
|
description: '',
|
||||||
|
mode: 'open' as const,
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectStore.create({
|
||||||
|
id: deletionProject,
|
||||||
|
name: 'Deletion Project',
|
||||||
|
description: '',
|
||||||
|
mode: 'open' as const,
|
||||||
|
});
|
||||||
|
|
||||||
|
await environmentService.addEnvironmentToProject(
|
||||||
|
environment,
|
||||||
|
project,
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
await environmentService.addEnvironmentToProject(
|
||||||
|
environment,
|
||||||
|
project2,
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
|
||||||
|
await featureToggleServiceV2.createFeatureToggle(
|
||||||
|
project,
|
||||||
|
{
|
||||||
|
name: feature1,
|
||||||
|
description: 'the #1 feature',
|
||||||
|
},
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
|
||||||
|
await featureToggleServiceV2.createStrategy(
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
constraints: [],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{ projectId: project, featureName: feature1, environment: DEFAULT_ENV },
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
await featureToggleServiceV2.createStrategy(
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
constraints: [],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{ projectId: project, featureName: feature1, environment },
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
|
||||||
|
// create feature 2
|
||||||
|
await featureToggleServiceV2.createFeatureToggle(
|
||||||
|
project,
|
||||||
|
{
|
||||||
|
name: feature2,
|
||||||
|
},
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
await featureToggleServiceV2.createStrategy(
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
constraints: [],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{ projectId: project, featureName: feature2, environment },
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
|
||||||
|
// create feature 3
|
||||||
|
await featureToggleServiceV2.createFeatureToggle(
|
||||||
|
project2,
|
||||||
|
{
|
||||||
|
name: feature3,
|
||||||
|
},
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
await featureToggleServiceV2.createStrategy(
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
constraints: [],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{ projectId: project2, featureName: feature3, environment },
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('doesnt return feature flags if project deleted', async () => {
|
||||||
|
const token = await apiTokenService.createApiToken({
|
||||||
|
type: ApiTokenType.CLIENT,
|
||||||
|
tokenName: deletionTokenName,
|
||||||
|
environment,
|
||||||
|
project: deletionProject,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.services.projectService.deleteProject(
|
||||||
|
deletionProject,
|
||||||
|
new User(TEST_AUDIT_USER),
|
||||||
|
TEST_AUDIT_USER,
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.services.apiTokenService.fetchActiveTokens();
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.get('/api/client/features')
|
||||||
|
.set('Authorization', token.secret)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(401);
|
||||||
|
});
|
@ -69,7 +69,7 @@ test('should only return valid tokens', async () => {
|
|||||||
|
|
||||||
const expiredToken = await stores.apiTokenStore.insert({
|
const expiredToken = await stores.apiTokenStore.insert({
|
||||||
tokenName: 'expired',
|
tokenName: 'expired',
|
||||||
secret: 'expired-secret',
|
secret: '*:environment.expired-secret',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
expiresAt: yesterday,
|
expiresAt: yesterday,
|
||||||
projects: ['*'],
|
projects: ['*'],
|
||||||
@ -78,7 +78,7 @@ test('should only return valid tokens', async () => {
|
|||||||
|
|
||||||
const activeToken = await stores.apiTokenStore.insert({
|
const activeToken = await stores.apiTokenStore.insert({
|
||||||
tokenName: 'default-valid',
|
tokenName: 'default-valid',
|
||||||
secret: 'valid-secret',
|
secret: '*:environment.valid-secret',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
expiresAt: tomorrow,
|
expiresAt: tomorrow,
|
||||||
projects: ['*'],
|
projects: ['*'],
|
||||||
|
Loading…
Reference in New Issue
Block a user