2021-03-29 19:58:11 +02:00
|
|
|
import crypto from 'crypto';
|
2021-04-22 23:40:52 +02:00
|
|
|
import { Logger } from '../logger';
|
2022-08-18 10:20:51 +02:00
|
|
|
import { ADMIN, CLIENT, FRONTEND } from '../types/permissions';
|
2022-11-28 10:56:34 +01:00
|
|
|
import { IEventStore, IUnleashStores } from '../types/stores';
|
2021-04-22 10:07:10 +02:00
|
|
|
import { IUnleashConfig } from '../types/option';
|
2021-04-22 23:40:52 +02:00
|
|
|
import ApiUser from '../types/api-user';
|
2021-08-12 15:04:37 +02:00
|
|
|
import {
|
|
|
|
ApiTokenType,
|
|
|
|
IApiToken,
|
2022-04-06 08:11:41 +02:00
|
|
|
ILegacyApiTokenCreate,
|
2021-09-15 20:28:10 +02:00
|
|
|
IApiTokenCreate,
|
2022-01-05 10:00:59 +01:00
|
|
|
validateApiToken,
|
2022-03-24 11:26:00 +01:00
|
|
|
validateApiTokenEnvironment,
|
2022-04-06 08:11:41 +02:00
|
|
|
mapLegacyToken,
|
|
|
|
mapLegacyTokenWithSecret,
|
2021-09-15 20:28:10 +02:00
|
|
|
} from '../types/models/api-token';
|
|
|
|
import { IApiTokenStore } from '../types/stores/api-token-store';
|
|
|
|
import { FOREIGN_KEY_VIOLATION } from '../error/db-error';
|
|
|
|
import BadDataError from '../error/bad-data-error';
|
2022-03-24 11:26:00 +01:00
|
|
|
import { IEnvironmentStore } from 'lib/types/stores/environment-store';
|
2022-08-19 10:48:33 +02:00
|
|
|
import { constantTimeCompare } from '../util/constantTimeCompare';
|
2022-11-28 10:56:34 +01:00
|
|
|
import {
|
|
|
|
ApiTokenCreatedEvent,
|
|
|
|
ApiTokenDeletedEvent,
|
|
|
|
ApiTokenUpdatedEvent,
|
|
|
|
} from '../types';
|
|
|
|
import { omitKeys } from '../util';
|
2021-03-29 19:58:11 +02:00
|
|
|
|
2022-08-16 15:33:33 +02:00
|
|
|
const resolveTokenPermissions = (tokenType: string) => {
|
|
|
|
if (tokenType === ApiTokenType.ADMIN) {
|
|
|
|
return [ADMIN];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tokenType === ApiTokenType.CLIENT) {
|
|
|
|
return [CLIENT];
|
|
|
|
}
|
|
|
|
|
2022-08-18 10:20:51 +02:00
|
|
|
if (tokenType === ApiTokenType.FRONTEND) {
|
|
|
|
return [FRONTEND];
|
2022-08-16 15:33:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return [];
|
|
|
|
};
|
|
|
|
|
2021-03-29 19:58:11 +02:00
|
|
|
export class ApiTokenService {
|
2021-08-12 15:04:37 +02:00
|
|
|
private store: IApiTokenStore;
|
2021-03-29 19:58:11 +02:00
|
|
|
|
2022-03-24 11:26:00 +01:00
|
|
|
private environmentStore: IEnvironmentStore;
|
|
|
|
|
2021-03-29 19:58:11 +02:00
|
|
|
private logger: Logger;
|
|
|
|
|
|
|
|
private activeTokens: IApiToken[] = [];
|
|
|
|
|
2022-11-28 10:56:34 +01:00
|
|
|
private eventStore: IEventStore;
|
|
|
|
|
2022-11-29 20:46:40 +01:00
|
|
|
private lastSeenSecrets: Set<string> = new Set<string>();
|
|
|
|
|
2021-04-22 10:07:10 +02:00
|
|
|
constructor(
|
2022-03-24 11:26:00 +01:00
|
|
|
{
|
|
|
|
apiTokenStore,
|
|
|
|
environmentStore,
|
2022-11-28 10:56:34 +01:00
|
|
|
eventStore,
|
|
|
|
}: Pick<
|
|
|
|
IUnleashStores,
|
|
|
|
'apiTokenStore' | 'environmentStore' | 'eventStore'
|
|
|
|
>,
|
2022-12-12 15:32:35 +01:00
|
|
|
config: Pick<IUnleashConfig, 'getLogger' | 'authentication'>,
|
2021-04-22 10:07:10 +02:00
|
|
|
) {
|
2021-08-12 15:04:37 +02:00
|
|
|
this.store = apiTokenStore;
|
2022-11-28 10:56:34 +01:00
|
|
|
this.eventStore = eventStore;
|
2022-03-24 11:26:00 +01:00
|
|
|
this.environmentStore = environmentStore;
|
2021-03-29 19:58:11 +02:00
|
|
|
this.logger = config.getLogger('/services/api-token-service.ts');
|
|
|
|
this.fetchActiveTokens();
|
2022-12-12 15:32:35 +01:00
|
|
|
this.updateLastSeen();
|
2022-01-05 10:00:59 +01:00
|
|
|
if (config.authentication.initApiTokens.length > 0) {
|
|
|
|
process.nextTick(async () =>
|
|
|
|
this.initApiTokens(config.authentication.initApiTokens),
|
|
|
|
);
|
|
|
|
}
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
2022-08-18 10:20:51 +02:00
|
|
|
async fetchActiveTokens(): Promise<void> {
|
2021-03-29 19:58:11 +02:00
|
|
|
try {
|
|
|
|
this.activeTokens = await this.getAllActiveTokens();
|
|
|
|
} finally {
|
|
|
|
// eslint-disable-next-line no-unsafe-finally
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-29 20:46:40 +01:00
|
|
|
async updateLastSeen(): Promise<void> {
|
|
|
|
if (this.lastSeenSecrets.size > 0) {
|
|
|
|
const toStore = [...this.lastSeenSecrets];
|
|
|
|
this.lastSeenSecrets = new Set<string>();
|
|
|
|
await this.store.markSeenAt(toStore);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-29 19:58:11 +02:00
|
|
|
public async getAllTokens(): Promise<IApiToken[]> {
|
|
|
|
return this.store.getAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getAllActiveTokens(): Promise<IApiToken[]> {
|
|
|
|
return this.store.getAllActive();
|
|
|
|
}
|
|
|
|
|
2022-04-06 08:11:41 +02:00
|
|
|
private async initApiTokens(tokens: ILegacyApiTokenCreate[]) {
|
2022-01-05 10:00:59 +01:00
|
|
|
const tokenCount = await this.store.count();
|
|
|
|
if (tokenCount > 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
2022-04-06 08:11:41 +02:00
|
|
|
const createAll = tokens
|
|
|
|
.map(mapLegacyTokenWithSecret)
|
2022-11-28 10:56:34 +01:00
|
|
|
.map((t) => this.insertNewApiToken(t, 'init-api-tokens'));
|
2022-01-05 10:00:59 +01:00
|
|
|
await Promise.all(createAll);
|
|
|
|
} catch (e) {
|
|
|
|
this.logger.error('Unable to create initial Admin API tokens');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-22 23:40:52 +02:00
|
|
|
public getUserForToken(secret: string): ApiUser | undefined {
|
2022-08-18 10:20:51 +02:00
|
|
|
if (!secret) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2022-08-19 10:48:33 +02:00
|
|
|
let token = this.activeTokens.find(
|
|
|
|
(activeToken) =>
|
|
|
|
Boolean(activeToken.secret) &&
|
|
|
|
constantTimeCompare(activeToken.secret, secret),
|
|
|
|
);
|
2022-08-17 10:55:52 +02:00
|
|
|
|
2022-08-19 10:48:33 +02:00
|
|
|
// If the token is not found, try to find it in the legacy format with alias.
|
|
|
|
// This allows us to support the old format of tokens migrating to the embedded proxy.
|
|
|
|
if (!token) {
|
2022-08-18 10:20:51 +02:00
|
|
|
token = this.activeTokens.find(
|
2022-08-19 10:48:33 +02:00
|
|
|
(activeToken) =>
|
|
|
|
Boolean(activeToken.alias) &&
|
|
|
|
constantTimeCompare(activeToken.alias, secret),
|
2022-08-18 10:20:51 +02:00
|
|
|
);
|
2022-08-17 10:55:52 +02:00
|
|
|
}
|
|
|
|
|
2021-03-29 19:58:11 +02:00
|
|
|
if (token) {
|
2022-12-12 15:32:35 +01:00
|
|
|
this.lastSeenSecrets.add(token.secret);
|
2022-11-29 20:46:40 +01:00
|
|
|
|
2021-04-22 23:40:52 +02:00
|
|
|
return new ApiUser({
|
2021-03-29 19:58:11 +02:00
|
|
|
username: token.username,
|
2022-08-16 15:33:33 +02:00
|
|
|
permissions: resolveTokenPermissions(token.type),
|
2022-04-06 08:11:41 +02:00
|
|
|
projects: token.projects,
|
2021-09-15 20:28:10 +02:00
|
|
|
environment: token.environment,
|
|
|
|
type: token.type,
|
2022-08-16 15:33:33 +02:00
|
|
|
secret: token.secret,
|
2021-03-29 19:58:11 +02:00
|
|
|
});
|
|
|
|
}
|
2022-08-19 10:48:33 +02:00
|
|
|
|
2021-03-29 19:58:11 +02:00
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async updateExpiry(
|
|
|
|
secret: string,
|
|
|
|
expiresAt: Date,
|
2022-11-28 10:56:34 +01:00
|
|
|
updatedBy: string,
|
2021-03-29 19:58:11 +02:00
|
|
|
): Promise<IApiToken> {
|
2022-11-28 10:56:34 +01:00
|
|
|
const previous = await this.store.get(secret);
|
|
|
|
const token = await this.store.setExpiry(secret, expiresAt);
|
|
|
|
await this.eventStore.store(
|
|
|
|
new ApiTokenUpdatedEvent({
|
|
|
|
createdBy: updatedBy,
|
|
|
|
previousToken: omitKeys(previous, 'secret'),
|
|
|
|
apiToken: omitKeys(token, 'secret'),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
return token;
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
2022-11-28 10:56:34 +01:00
|
|
|
public async delete(secret: string, deletedBy: string): Promise<void> {
|
|
|
|
if (await this.store.exists(secret)) {
|
|
|
|
const token = await this.store.get(secret);
|
|
|
|
await this.store.delete(secret);
|
|
|
|
await this.eventStore.store(
|
|
|
|
new ApiTokenDeletedEvent({
|
|
|
|
createdBy: deletedBy,
|
|
|
|
apiToken: omitKeys(token, 'secret'),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
2022-04-06 08:11:41 +02:00
|
|
|
/**
|
|
|
|
* @deprecated This may be removed in a future release, prefer createApiTokenWithProjects
|
|
|
|
*/
|
2021-09-15 20:28:10 +02:00
|
|
|
public async createApiToken(
|
2022-04-06 08:11:41 +02:00
|
|
|
newToken: Omit<ILegacyApiTokenCreate, 'secret'>,
|
2022-11-28 10:56:34 +01:00
|
|
|
createdBy: string = 'unleash-system',
|
2022-04-06 08:11:41 +02:00
|
|
|
): Promise<IApiToken> {
|
|
|
|
const token = mapLegacyToken(newToken);
|
2022-11-28 10:56:34 +01:00
|
|
|
return this.createApiTokenWithProjects(token, createdBy);
|
2022-04-06 08:11:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async createApiTokenWithProjects(
|
2021-09-15 20:28:10 +02:00
|
|
|
newToken: Omit<IApiTokenCreate, 'secret'>,
|
2022-11-28 10:56:34 +01:00
|
|
|
createdBy: string = 'unleash-system',
|
2021-03-29 19:58:11 +02:00
|
|
|
): Promise<IApiToken> {
|
2022-01-05 10:00:59 +01:00
|
|
|
validateApiToken(newToken);
|
2021-09-15 20:28:10 +02:00
|
|
|
|
2022-03-24 11:26:00 +01:00
|
|
|
const environments = await this.environmentStore.getAll();
|
|
|
|
validateApiTokenEnvironment(newToken, environments);
|
|
|
|
|
2021-09-15 20:28:10 +02:00
|
|
|
const secret = this.generateSecretKey(newToken);
|
|
|
|
const createNewToken = { ...newToken, secret };
|
2022-11-28 10:56:34 +01:00
|
|
|
return this.insertNewApiToken(createNewToken, createdBy);
|
2022-01-05 10:00:59 +01:00
|
|
|
}
|
2021-09-15 20:28:10 +02:00
|
|
|
|
2022-09-12 15:22:23 +02:00
|
|
|
// TODO: Remove this service method after embedded proxy has been released in
|
|
|
|
// 4.16.0
|
|
|
|
public async createMigratedProxyApiToken(
|
|
|
|
newToken: Omit<IApiTokenCreate, 'secret'>,
|
|
|
|
): Promise<IApiToken> {
|
|
|
|
validateApiToken(newToken);
|
|
|
|
|
|
|
|
const secret = this.generateSecretKey(newToken);
|
|
|
|
const createNewToken = { ...newToken, secret };
|
2022-11-28 10:56:34 +01:00
|
|
|
return this.insertNewApiToken(createNewToken, 'system-migration');
|
2022-09-12 15:22:23 +02:00
|
|
|
}
|
|
|
|
|
2022-01-05 10:00:59 +01:00
|
|
|
private async insertNewApiToken(
|
|
|
|
newApiToken: IApiTokenCreate,
|
2022-11-28 10:56:34 +01:00
|
|
|
createdBy: string,
|
2022-01-05 10:00:59 +01:00
|
|
|
): Promise<IApiToken> {
|
2021-09-15 20:28:10 +02:00
|
|
|
try {
|
2022-01-05 10:00:59 +01:00
|
|
|
const token = await this.store.insert(newApiToken);
|
2021-09-15 20:28:10 +02:00
|
|
|
this.activeTokens.push(token);
|
2022-11-28 10:56:34 +01:00
|
|
|
await this.eventStore.store(
|
|
|
|
new ApiTokenCreatedEvent({
|
|
|
|
createdBy,
|
|
|
|
apiToken: omitKeys(token, 'secret'),
|
|
|
|
}),
|
|
|
|
);
|
2021-09-15 20:28:10 +02:00
|
|
|
return token;
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code === FOREIGN_KEY_VIOLATION) {
|
|
|
|
let { message } = error;
|
2022-04-06 08:11:41 +02:00
|
|
|
if (error.constraint === 'api_token_project_project_fkey') {
|
|
|
|
message = `Project=${this.findInvalidProject(
|
|
|
|
error.detail,
|
|
|
|
newApiToken.projects,
|
|
|
|
)} does not exist`;
|
2021-09-15 20:28:10 +02:00
|
|
|
} else if (error.constraint === 'api_tokens_environment_fkey') {
|
2022-01-05 10:00:59 +01:00
|
|
|
message = `Environment=${newApiToken.environment} does not exist`;
|
2021-09-15 20:28:10 +02:00
|
|
|
}
|
|
|
|
throw new BadDataError(message);
|
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
}
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
2022-04-06 08:11:41 +02:00
|
|
|
private findInvalidProject(errorDetails, projects) {
|
|
|
|
if (!errorDetails) {
|
|
|
|
return 'invalid';
|
|
|
|
}
|
|
|
|
let invalidProject = projects.find((project) => {
|
|
|
|
return errorDetails.includes(`=(${project})`);
|
|
|
|
});
|
|
|
|
return invalidProject || 'invalid';
|
|
|
|
}
|
|
|
|
|
|
|
|
private generateSecretKey({ projects, environment }) {
|
2021-09-15 20:28:10 +02:00
|
|
|
const randomStr = crypto.randomBytes(28).toString('hex');
|
2022-04-06 08:11:41 +02:00
|
|
|
if (projects.length > 1) {
|
|
|
|
return `[]:${environment}.${randomStr}`;
|
|
|
|
} else {
|
|
|
|
return `${projects[0]}:${environment}.${randomStr}`;
|
|
|
|
}
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
}
|