mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-05 17:53:12 +02:00
feat: add proxy token
This commit is contained in:
parent
449167b17d
commit
b13df231fa
@ -9,12 +9,13 @@ import {
|
|||||||
ApiTokenType,
|
ApiTokenType,
|
||||||
IApiToken,
|
IApiToken,
|
||||||
IApiTokenCreate,
|
IApiTokenCreate,
|
||||||
isAllProjects,
|
isAll,
|
||||||
} from '../types/models/api-token';
|
} from '../types/models/api-token';
|
||||||
import { ALL_PROJECTS } from '../util/constants';
|
import { ALL_PROJECTS } from '../util/constants';
|
||||||
|
|
||||||
const TABLE = 'api_tokens';
|
const TABLE = 'api_tokens';
|
||||||
const API_LINK_TABLE = 'api_token_project';
|
const API_LINK_TABLE_PROJECT = 'api_token_project';
|
||||||
|
const API_LINK_TABLE_ENVIRONMENT = 'api_token_environment';
|
||||||
|
|
||||||
const ALL = '*';
|
const ALL = '*';
|
||||||
|
|
||||||
@ -26,15 +27,15 @@ interface ITokenInsert {
|
|||||||
expires_at?: Date;
|
expires_at?: Date;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
seen_at?: Date;
|
seen_at?: Date;
|
||||||
environment: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ITokenRow extends ITokenInsert {
|
interface ITokenRow extends ITokenInsert {
|
||||||
project: string;
|
project: string;
|
||||||
|
environment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenRowReducer = (acc, tokenRow) => {
|
const tokenRowReducer = (acc, tokenRow) => {
|
||||||
const { project, ...token } = tokenRow;
|
const { project, environment, ...token } = tokenRow;
|
||||||
if (!acc[tokenRow.secret]) {
|
if (!acc[tokenRow.secret]) {
|
||||||
acc[tokenRow.secret] = {
|
acc[tokenRow.secret] = {
|
||||||
secret: token.secret,
|
secret: token.secret,
|
||||||
@ -42,19 +43,27 @@ const tokenRowReducer = (acc, tokenRow) => {
|
|||||||
type: token.type,
|
type: token.type,
|
||||||
project: ALL,
|
project: ALL,
|
||||||
projects: [ALL],
|
projects: [ALL],
|
||||||
environment: token.environment ? token.environment : ALL,
|
environment: ALL,
|
||||||
|
environments: [ALL],
|
||||||
expiresAt: token.expires_at,
|
expiresAt: token.expires_at,
|
||||||
createdAt: token.created_at,
|
createdAt: token.created_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const currentToken = acc[tokenRow.secret];
|
const currentToken = acc[tokenRow.secret];
|
||||||
if (tokenRow.project) {
|
if (tokenRow.project) {
|
||||||
if (isAllProjects(currentToken.projects)) {
|
if (isAll(currentToken.projects)) {
|
||||||
currentToken.projects = [];
|
currentToken.projects = [];
|
||||||
}
|
}
|
||||||
currentToken.projects.push(tokenRow.project);
|
currentToken.projects.push(tokenRow.project);
|
||||||
currentToken.project = currentToken.projects.join(',');
|
currentToken.project = currentToken.projects.join(',');
|
||||||
}
|
}
|
||||||
|
if (tokenRow.environment) {
|
||||||
|
if (isAll(currentToken.environments)) {
|
||||||
|
currentToken.environments = [];
|
||||||
|
}
|
||||||
|
currentToken.environments.push(tokenRow.environment);
|
||||||
|
currentToken.environment = currentToken.environments.join(',');
|
||||||
|
}
|
||||||
return acc;
|
return acc;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -62,8 +71,9 @@ const toRow = (newToken: IApiTokenCreate) => ({
|
|||||||
username: newToken.username,
|
username: newToken.username,
|
||||||
secret: newToken.secret,
|
secret: newToken.secret,
|
||||||
type: newToken.type,
|
type: newToken.type,
|
||||||
environment:
|
environment: isAll(newToken.environments)
|
||||||
newToken.environment === ALL ? undefined : newToken.environment,
|
? undefined
|
||||||
|
: newToken.environments,
|
||||||
expires_at: newToken.expiresAt,
|
expires_at: newToken.expiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,10 +124,15 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
private makeTokenProjectQuery() {
|
private makeTokenProjectQuery() {
|
||||||
return this.db<ITokenRow>(`${TABLE} as tokens`)
|
return this.db<ITokenRow>(`${TABLE} as tokens`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
`${API_LINK_TABLE} as token_project_link`,
|
`${API_LINK_TABLE_PROJECT} as token_project_link`,
|
||||||
'tokens.secret',
|
'tokens.secret',
|
||||||
'token_project_link.secret',
|
'token_project_link.secret',
|
||||||
)
|
)
|
||||||
|
.leftJoin(
|
||||||
|
`${API_LINK_TABLE_ENVIRONMENT} as token_environment_link`,
|
||||||
|
'tokens.secret',
|
||||||
|
'token_environment_link.secret',
|
||||||
|
)
|
||||||
.select(
|
.select(
|
||||||
'tokens.secret',
|
'tokens.secret',
|
||||||
'username',
|
'username',
|
||||||
@ -125,8 +140,8 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
'expires_at',
|
'expires_at',
|
||||||
'created_at',
|
'created_at',
|
||||||
'seen_at',
|
'seen_at',
|
||||||
'environment',
|
|
||||||
'token_project_link.project',
|
'token_project_link.project',
|
||||||
|
'token_environment_link.environment',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,14 +158,26 @@ export class ApiTokenStore implements IApiTokenStore {
|
|||||||
})
|
})
|
||||||
.map((project) => {
|
.map((project) => {
|
||||||
return tx.raw(
|
return tx.raw(
|
||||||
`INSERT INTO ${API_LINK_TABLE} VALUES (?, ?)`,
|
`INSERT INTO ${API_LINK_TABLE_PROJECT} VALUES (?, ?)`,
|
||||||
[newToken.secret, project],
|
[newToken.secret, project],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await Promise.all(updateProjectTasks);
|
await Promise.all(updateProjectTasks);
|
||||||
|
const updateEnvironmentTasks = (newToken.environments || [])
|
||||||
|
.filter((environment) => {
|
||||||
|
return environment !== ALL;
|
||||||
|
})
|
||||||
|
.map((environment) => {
|
||||||
|
return tx.raw(
|
||||||
|
`INSERT INTO ${API_LINK_TABLE_ENVIRONMENT} VALUES (?, ?)`,
|
||||||
|
[newToken.secret, environment],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Promise.all(updateEnvironmentTasks);
|
||||||
return {
|
return {
|
||||||
...newToken,
|
...newToken,
|
||||||
project: newToken.projects?.join(',') || '*',
|
project: newToken.projects?.join(',') || '*',
|
||||||
|
environment: newToken.environments?.join(',') || '*',
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ import { IFeatureToggleQuery, ISegment } from '../../types/model';
|
|||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../unleash-types';
|
||||||
import ApiUser from '../../types/api-user';
|
import ApiUser from '../../types/api-user';
|
||||||
import { ALL, isAllProjects } from '../../types/models/api-token';
|
import { isAll } from '../../types/models/api-token';
|
||||||
import { SegmentService } from '../../services/segment-service';
|
import { SegmentService } from '../../services/segment-service';
|
||||||
import { FeatureConfigurationClient } from '../../types/stores/feature-strategies-store';
|
import { FeatureConfigurationClient } from '../../types/stores/feature-strategies-store';
|
||||||
import { ClientSpecService } from '../../services/client-spec-service';
|
import { ClientSpecService } from '../../services/client-spec-service';
|
||||||
@ -30,7 +30,7 @@ const version = 2;
|
|||||||
|
|
||||||
interface QueryOverride {
|
interface QueryOverride {
|
||||||
project?: string[];
|
project?: string[];
|
||||||
environment?: string;
|
environment?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FeatureController extends Controller {
|
export default class FeatureController extends Controller {
|
||||||
@ -135,11 +135,11 @@ export default class FeatureController extends Controller {
|
|||||||
|
|
||||||
const override: QueryOverride = {};
|
const override: QueryOverride = {};
|
||||||
if (user instanceof ApiUser) {
|
if (user instanceof ApiUser) {
|
||||||
if (!isAllProjects(user.projects)) {
|
if (!isAll(user.projects)) {
|
||||||
override.project = user.projects;
|
override.project = user.projects;
|
||||||
}
|
}
|
||||||
if (user.environment !== ALL) {
|
if (!isAll(user.environments)) {
|
||||||
override.environment = user.environment;
|
override.environment = user.environments;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import ClientInstanceService from '../../services/client-metrics/instance-servic
|
|||||||
import { IAuthRequest, User } from '../../server-impl';
|
import { IAuthRequest, User } from '../../server-impl';
|
||||||
import { IClientApp } from '../../types/model';
|
import { IClientApp } from '../../types/model';
|
||||||
import ApiUser from '../../types/api-user';
|
import ApiUser from '../../types/api-user';
|
||||||
import { ALL } from '../../types/models/api-token';
|
import { isAll } from '../../types/models/api-token';
|
||||||
import { NONE } from '../../types/permissions';
|
import { NONE } from '../../types/permissions';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
import { emptyResponse } from '../../openapi/util/standard-responses';
|
import { emptyResponse } from '../../openapi/util/standard-responses';
|
||||||
@ -51,9 +51,9 @@ export default class RegisterController extends Controller {
|
|||||||
|
|
||||||
private static resolveEnvironment(user: User, data: Partial<IClientApp>) {
|
private static resolveEnvironment(user: User, data: Partial<IClientApp>) {
|
||||||
if (user instanceof ApiUser) {
|
if (user instanceof ApiUser) {
|
||||||
if (user.environment !== ALL) {
|
if (!isAll(user.environments)) {
|
||||||
return user.environment;
|
return user.environments;
|
||||||
} else if (user.environment === ALL && data.environment) {
|
} else if (isAll(user.environments) && data.environment) {
|
||||||
return data.environment;
|
return data.environment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ export default class RegisterController extends Controller {
|
|||||||
res: Response<void>,
|
res: Response<void>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { body: data, ip: clientIp, user } = req;
|
const { body: data, ip: clientIp, user } = req;
|
||||||
data.environment = RegisterController.resolveEnvironment(user, data);
|
data.environments = RegisterController.resolveEnvironment(user, data);
|
||||||
await this.clientInstanceService.registerClient(data, clientIp);
|
await this.clientInstanceService.registerClient(data, clientIp);
|
||||||
return res.status(202).end();
|
return res.status(202).end();
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ export const createApiToken = joi
|
|||||||
.string()
|
.string()
|
||||||
.lowercase()
|
.lowercase()
|
||||||
.required()
|
.required()
|
||||||
.valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT),
|
.valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT, ApiTokenType.PROXY),
|
||||||
expiresAt: joi.date().optional(),
|
expiresAt: joi.date().optional(),
|
||||||
project: joi.when('projects', {
|
project: joi.when('projects', {
|
||||||
not: joi.required(),
|
not: joi.required(),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { ADMIN, CLIENT, PROXY } from '../types/permissions';
|
import { ADMIN, PROXY, CLIENT } from '../types/permissions';
|
||||||
import { IUnleashStores } from '../types/stores';
|
import { IUnleashStores } from '../types/stores';
|
||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
import ApiUser from '../types/api-user';
|
import ApiUser from '../types/api-user';
|
||||||
@ -108,7 +108,7 @@ export class ApiTokenService {
|
|||||||
username: token.username,
|
username: token.username,
|
||||||
permissions: resolveTokenPermissions(token.type),
|
permissions: resolveTokenPermissions(token.type),
|
||||||
projects: token.projects,
|
projects: token.projects,
|
||||||
environment: token.environment,
|
environments: token.environments,
|
||||||
type: token.type,
|
type: token.type,
|
||||||
secret: token.secret,
|
secret: token.secret,
|
||||||
});
|
});
|
||||||
@ -161,12 +161,15 @@ export class ApiTokenService {
|
|||||||
if (error.code === FOREIGN_KEY_VIOLATION) {
|
if (error.code === FOREIGN_KEY_VIOLATION) {
|
||||||
let { message } = error;
|
let { message } = error;
|
||||||
if (error.constraint === 'api_token_project_project_fkey') {
|
if (error.constraint === 'api_token_project_project_fkey') {
|
||||||
message = `Project=${this.findInvalidProject(
|
message = `Project=${this.findInvalid(
|
||||||
error.detail,
|
error.detail,
|
||||||
newApiToken.projects,
|
newApiToken.projects,
|
||||||
)} does not exist`;
|
)} does not exist`;
|
||||||
} else if (error.constraint === 'api_tokens_environment_fkey') {
|
} else if (error.constraint === 'api_tokens_environment_fkey') {
|
||||||
message = `Environment=${newApiToken.environment} does not exist`;
|
message = `Environment=${this.findInvalid(
|
||||||
|
error.detail,
|
||||||
|
newApiToken.environments,
|
||||||
|
)} does not exist`;
|
||||||
}
|
}
|
||||||
throw new BadDataError(message);
|
throw new BadDataError(message);
|
||||||
}
|
}
|
||||||
@ -174,23 +177,21 @@ export class ApiTokenService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private findInvalidProject(errorDetails, projects) {
|
private findInvalid(errorDetails, values) {
|
||||||
if (!errorDetails) {
|
if (!errorDetails) {
|
||||||
return 'invalid';
|
return 'invalid';
|
||||||
}
|
}
|
||||||
let invalidProject = projects.find((project) => {
|
let invalid = values.find((value) => {
|
||||||
return errorDetails.includes(`=(${project})`);
|
return errorDetails.includes(`=(${value})`);
|
||||||
});
|
});
|
||||||
return invalidProject || 'invalid';
|
return invalid || 'invalid';
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateSecretKey({ projects, environment }) {
|
private generateSecretKey({ projects, environments }) {
|
||||||
const randomStr = crypto.randomBytes(28).toString('hex');
|
const randomStr = crypto.randomBytes(28).toString('hex');
|
||||||
if (projects.length > 1) {
|
return `${projects.length > 1 ? '[]' : projects[0]}:${
|
||||||
return `[]:${environment}.${randomStr}`;
|
environments.length > 1 ? '[]' : environments[0]
|
||||||
} else {
|
}.${randomStr}`;
|
||||||
return `${projects[0]}:${environment}.${randomStr}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
|
@ -6,7 +6,8 @@ interface IApiUserData {
|
|||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
projects?: string[];
|
projects?: string[];
|
||||||
project?: string;
|
project?: string;
|
||||||
environment: string;
|
environments?: string[];
|
||||||
|
environment?: string;
|
||||||
type: ApiTokenType;
|
type: ApiTokenType;
|
||||||
secret: string;
|
secret: string;
|
||||||
}
|
}
|
||||||
@ -20,7 +21,7 @@ export default class ApiUser {
|
|||||||
|
|
||||||
readonly projects: string[];
|
readonly projects: string[];
|
||||||
|
|
||||||
readonly environment: string;
|
readonly environments: string[];
|
||||||
|
|
||||||
readonly type: ApiTokenType;
|
readonly type: ApiTokenType;
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ export default class ApiUser {
|
|||||||
permissions = [CLIENT],
|
permissions = [CLIENT],
|
||||||
projects,
|
projects,
|
||||||
project,
|
project,
|
||||||
|
environments,
|
||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
secret,
|
secret,
|
||||||
@ -40,7 +42,6 @@ export default class ApiUser {
|
|||||||
}
|
}
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.permissions = permissions;
|
this.permissions = permissions;
|
||||||
this.environment = environment;
|
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.secret = secret;
|
this.secret = secret;
|
||||||
if (projects && projects.length > 0) {
|
if (projects && projects.length > 0) {
|
||||||
@ -48,5 +49,10 @@ export default class ApiUser {
|
|||||||
} else {
|
} else {
|
||||||
this.projects = [project];
|
this.projects = [project];
|
||||||
}
|
}
|
||||||
|
if (environments && environments.length > 0) {
|
||||||
|
this.environments = environments;
|
||||||
|
} else {
|
||||||
|
this.environments = [environment];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,8 @@ export interface ILegacyApiTokenCreate {
|
|||||||
secret: string;
|
secret: string;
|
||||||
username: string;
|
username: string;
|
||||||
type: ApiTokenType;
|
type: ApiTokenType;
|
||||||
environment: string;
|
environment?: string;
|
||||||
|
environments?: string[];
|
||||||
project?: string;
|
project?: string;
|
||||||
projects?: string[];
|
projects?: string[];
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
@ -23,7 +24,7 @@ export interface IApiTokenCreate {
|
|||||||
secret: string;
|
secret: string;
|
||||||
username: string;
|
username: string;
|
||||||
type: ApiTokenType;
|
type: ApiTokenType;
|
||||||
environment: string;
|
environments: string[];
|
||||||
projects: string[];
|
projects: string[];
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
}
|
}
|
||||||
@ -35,38 +36,48 @@ export interface IApiToken extends IApiTokenCreate {
|
|||||||
project: string;
|
project: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isAllProjects = (projects: string[]): boolean => {
|
export const isAll = (values: string[]): boolean => {
|
||||||
return projects && projects.length === 1 && projects[0] === ALL;
|
return values && values.length === 1 && values[0] === ALL;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapLegacyProjects = (
|
export const mapLegacy = (
|
||||||
project?: string,
|
error: string,
|
||||||
projects?: string[],
|
value?: string,
|
||||||
|
values?: string[],
|
||||||
): string[] => {
|
): string[] => {
|
||||||
let cleanedProjects;
|
let cleanedValues;
|
||||||
if (project) {
|
if (value) {
|
||||||
cleanedProjects = [project];
|
cleanedValues = [value];
|
||||||
} else if (projects) {
|
} else if (values) {
|
||||||
cleanedProjects = projects;
|
cleanedValues = values;
|
||||||
if (cleanedProjects.includes('*')) {
|
if (cleanedValues.includes('*')) {
|
||||||
cleanedProjects = ['*'];
|
cleanedValues = ['*'];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new BadDataError(
|
throw new BadDataError(
|
||||||
'API tokens must either contain a project or projects field',
|
'API tokens must either contain an environment or environments field',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return cleanedProjects;
|
return cleanedValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapLegacyToken = (
|
export const mapLegacyToken = (
|
||||||
token: Omit<ILegacyApiTokenCreate, 'secret'>,
|
token: Omit<ILegacyApiTokenCreate, 'secret'>,
|
||||||
): Omit<IApiTokenCreate, 'secret'> => {
|
): Omit<IApiTokenCreate, 'secret'> => {
|
||||||
const cleanedProjects = mapLegacyProjects(token.project, token.projects);
|
const cleanedProjects = mapLegacy(
|
||||||
|
'API tokens must either contain a project or projects field',
|
||||||
|
token.project,
|
||||||
|
token.projects,
|
||||||
|
);
|
||||||
|
const cleanedEnvironments = mapLegacy(
|
||||||
|
'API tokens must either contain an environment or environments field',
|
||||||
|
token.environment,
|
||||||
|
token.environments,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
username: token.username,
|
username: token.username,
|
||||||
type: token.type,
|
type: token.type,
|
||||||
environment: token.environment,
|
environments: cleanedEnvironments,
|
||||||
projects: cleanedProjects,
|
projects: cleanedProjects,
|
||||||
expiresAt: token.expiresAt,
|
expiresAt: token.expiresAt,
|
||||||
};
|
};
|
||||||
@ -84,27 +95,27 @@ export const mapLegacyTokenWithSecret = (
|
|||||||
export const validateApiToken = ({
|
export const validateApiToken = ({
|
||||||
type,
|
type,
|
||||||
projects,
|
projects,
|
||||||
environment,
|
environments,
|
||||||
}: Omit<IApiTokenCreate, 'secret'>): void => {
|
}: Omit<IApiTokenCreate, 'secret'>): void => {
|
||||||
if (type === ApiTokenType.ADMIN && !isAllProjects(projects)) {
|
if (type === ApiTokenType.ADMIN && !isAll(projects)) {
|
||||||
throw new BadDataError(
|
throw new BadDataError(
|
||||||
'Admin token cannot be scoped to single project',
|
'Admin token cannot be scoped to single project',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === ApiTokenType.ADMIN && environment !== ALL) {
|
if (type === ApiTokenType.ADMIN && !isAll(environments)) {
|
||||||
throw new BadDataError(
|
throw new BadDataError(
|
||||||
'Admin token cannot be scoped to single environment',
|
'Admin token cannot be scoped to single environment',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === ApiTokenType.CLIENT && environment === ALL) {
|
if (type === ApiTokenType.CLIENT && isAll(environments)) {
|
||||||
throw new BadDataError(
|
throw new BadDataError(
|
||||||
'Client token cannot be scoped to all environments',
|
'Client token cannot be scoped to all environments',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === ApiTokenType.PROXY && environment === ALL) {
|
if (type === ApiTokenType.PROXY && isAll(environments)) {
|
||||||
throw new BadDataError(
|
throw new BadDataError(
|
||||||
'Proxy token cannot be scoped to all environments',
|
'Proxy token cannot be scoped to all environments',
|
||||||
);
|
);
|
||||||
@ -112,24 +123,26 @@ export const validateApiToken = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const validateApiTokenEnvironment = (
|
export const validateApiTokenEnvironment = (
|
||||||
{ environment }: Pick<IApiTokenCreate, 'environment'>,
|
{ environments }: Pick<IApiTokenCreate, 'environments'>,
|
||||||
environments: IEnvironment[],
|
allEnvironments: IEnvironment[],
|
||||||
): void => {
|
): void => {
|
||||||
if (environment === ALL) {
|
if (isAll(environments)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedEnvironment = environments.find(
|
const foundEnvironments = allEnvironments.filter((environment) =>
|
||||||
(env) => env.name === environment,
|
environments.includes(environment.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!selectedEnvironment) {
|
if (foundEnvironments.length !== environments.length) {
|
||||||
throw new BadDataError(`Environment=${environment} does not exist`);
|
throw new BadDataError('One or more environments do not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedEnvironment.enabled) {
|
if (
|
||||||
|
foundEnvironments.filter((environment) => !environment.enabled).length
|
||||||
|
) {
|
||||||
throw new BadDataError(
|
throw new BadDataError(
|
||||||
'Client token cannot be scoped to disabled environments',
|
'Token cannot be scoped to disabled environments',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
//Special
|
//Special
|
||||||
export const ADMIN = 'ADMIN';
|
export const ADMIN = 'ADMIN';
|
||||||
export const CLIENT = 'CLIENT';
|
|
||||||
export const PROXY = 'PROXY';
|
export const PROXY = 'PROXY';
|
||||||
|
export const CLIENT = 'CLIENT';
|
||||||
export const NONE = 'NONE';
|
export const NONE = 'NONE';
|
||||||
|
|
||||||
export const CREATE_FEATURE = 'CREATE_FEATURE';
|
export const CREATE_FEATURE = 'CREATE_FEATURE';
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
CREATE TABLE IF NOT EXISTS api_token_environment
|
||||||
|
(
|
||||||
|
secret text NOT NULL,
|
||||||
|
environment text NOT NULL,
|
||||||
|
FOREIGN KEY (secret) REFERENCES api_tokens (secret) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (environment) REFERENCES environments(name) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO api_token_environment SELECT secret, environment FROM api_tokens WHERE environment IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE api_tokens DROP COLUMN "environment";
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//This is a lossy down migration, tokens with multiple environments are discarded
|
||||||
|
exports.down = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
ALTER TABLE api_tokens ADD COLUMN environment VARCHAR REFERENCES ENVIRONMENTS(name) ON DELETE CASCADE;
|
||||||
|
DELETE FROM api_tokens WHERE secret LIKE '[]%';
|
||||||
|
|
||||||
|
UPDATE api_tokens
|
||||||
|
SET environment = subquery.environment
|
||||||
|
FROM(
|
||||||
|
SELECT token.secret, link.environment FROM api_tokens AS token LEFT JOIN api_token_environment AS link ON
|
||||||
|
token.secret = link.secret
|
||||||
|
) AS subquery
|
||||||
|
WHERE api_tokens.environment = subquery.environment;
|
||||||
|
|
||||||
|
DROP TABLE api_token_environment;
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
@ -26,7 +26,7 @@ beforeAll(async () => {
|
|||||||
token = await apiTokenService.createApiTokenWithProjects({
|
token = await apiTokenService.createApiTokenWithProjects({
|
||||||
type: ApiTokenType.ADMIN,
|
type: ApiTokenType.ADMIN,
|
||||||
username: 'tester',
|
username: 'tester',
|
||||||
environment: ALL,
|
environments: [ALL],
|
||||||
projects: [ALL],
|
projects: [ALL],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -386,7 +386,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () =>
|
|||||||
await app.services.apiTokenService.createApiTokenWithProjects({
|
await app.services.apiTokenService.createApiTokenWithProjects({
|
||||||
username: apiTokenName,
|
username: apiTokenName,
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
environment: environment,
|
environments: [environment],
|
||||||
projects: [projectId],
|
projects: [projectId],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -425,7 +425,7 @@ test(`should clean apitokens for not existing environment after import with drop
|
|||||||
await app.services.apiTokenService.createApiTokenWithProjects({
|
await app.services.apiTokenService.createApiTokenWithProjects({
|
||||||
username: apiTokenName,
|
username: apiTokenName,
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
environment: environment,
|
environments: [environment],
|
||||||
projects: [projectId],
|
projects: [projectId],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ test('should not partially create token if projects are invalid', async () => {
|
|||||||
username: 'default-client',
|
username: 'default-client',
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
projects: ['non-existent-project'],
|
projects: ['non-existent-project'],
|
||||||
environment: DEFAULT_ENV,
|
environments: [DEFAULT_ENV],
|
||||||
});
|
});
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
const allTokens = await apiTokenService.getAllTokens();
|
const allTokens = await apiTokenService.getAllTokens();
|
||||||
|
1
src/test/fixtures/fake-api-token-store.ts
vendored
1
src/test/fixtures/fake-api-token-store.ts
vendored
@ -51,6 +51,7 @@ export default class FakeApiTokenStore
|
|||||||
const apiToken = {
|
const apiToken = {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
project: newToken.projects?.join(',') || '*',
|
project: newToken.projects?.join(',') || '*',
|
||||||
|
environment: newToken.environments?.join(',') || '*',
|
||||||
...newToken,
|
...newToken,
|
||||||
};
|
};
|
||||||
this.tokens.push(apiToken);
|
this.tokens.push(apiToken);
|
||||||
|
Loading…
Reference in New Issue
Block a user