2021-03-29 19:58:11 +02:00
|
|
|
import crypto from 'crypto';
|
2021-04-22 23:40:52 +02:00
|
|
|
import { Logger } from '../logger';
|
2021-05-02 20:58:02 +02:00
|
|
|
import { ADMIN, CLIENT } from '../types/permissions';
|
2021-04-22 10:07:10 +02:00
|
|
|
import { IUnleashStores } from '../types/stores';
|
|
|
|
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 {
|
2021-09-15 20:28:10 +02:00
|
|
|
ALL,
|
2021-08-12 15:04:37 +02:00
|
|
|
ApiTokenType,
|
|
|
|
IApiToken,
|
2021-09-15 20:28:10 +02:00
|
|
|
IApiTokenCreate,
|
|
|
|
} 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';
|
2021-03-29 19:58:11 +02:00
|
|
|
|
|
|
|
const ONE_MINUTE = 60_000;
|
|
|
|
|
|
|
|
export class ApiTokenService {
|
2021-08-12 15:04:37 +02:00
|
|
|
private store: IApiTokenStore;
|
2021-03-29 19:58:11 +02:00
|
|
|
|
|
|
|
private logger: Logger;
|
|
|
|
|
|
|
|
private timer: NodeJS.Timeout;
|
|
|
|
|
|
|
|
private activeTokens: IApiToken[] = [];
|
|
|
|
|
2021-04-22 10:07:10 +02:00
|
|
|
constructor(
|
2021-08-12 15:04:37 +02:00
|
|
|
{ apiTokenStore }: Pick<IUnleashStores, 'apiTokenStore'>,
|
2021-04-22 10:07:10 +02:00
|
|
|
config: Pick<IUnleashConfig, 'getLogger'>,
|
|
|
|
) {
|
2021-08-12 15:04:37 +02:00
|
|
|
this.store = apiTokenStore;
|
2021-03-29 19:58:11 +02:00
|
|
|
this.logger = config.getLogger('/services/api-token-service.ts');
|
|
|
|
this.fetchActiveTokens();
|
|
|
|
this.timer = setInterval(
|
|
|
|
() => this.fetchActiveTokens(),
|
|
|
|
ONE_MINUTE,
|
|
|
|
).unref();
|
|
|
|
}
|
|
|
|
|
|
|
|
private async fetchActiveTokens(): Promise<void> {
|
|
|
|
try {
|
|
|
|
this.activeTokens = await this.getAllActiveTokens();
|
|
|
|
} finally {
|
|
|
|
// eslint-disable-next-line no-unsafe-finally
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getAllTokens(): Promise<IApiToken[]> {
|
|
|
|
return this.store.getAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getAllActiveTokens(): Promise<IApiToken[]> {
|
|
|
|
return this.store.getAllActive();
|
|
|
|
}
|
|
|
|
|
2021-04-22 23:40:52 +02:00
|
|
|
public getUserForToken(secret: string): ApiUser | undefined {
|
2021-08-12 15:04:37 +02:00
|
|
|
const token = this.activeTokens.find((t) => t.secret === secret);
|
2021-03-29 19:58:11 +02:00
|
|
|
if (token) {
|
|
|
|
const permissions =
|
|
|
|
token.type === ApiTokenType.ADMIN ? [ADMIN] : [CLIENT];
|
|
|
|
|
2021-04-22 23:40:52 +02:00
|
|
|
return new ApiUser({
|
2021-03-29 19:58:11 +02:00
|
|
|
username: token.username,
|
|
|
|
permissions,
|
2021-09-15 20:28:10 +02:00
|
|
|
project: token.project,
|
|
|
|
environment: token.environment,
|
|
|
|
type: token.type,
|
2021-03-29 19:58:11 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async updateExpiry(
|
|
|
|
secret: string,
|
|
|
|
expiresAt: Date,
|
|
|
|
): Promise<IApiToken> {
|
|
|
|
return this.store.setExpiry(secret, expiresAt);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async delete(secret: string): Promise<void> {
|
|
|
|
return this.store.delete(secret);
|
|
|
|
}
|
|
|
|
|
2021-09-15 20:28:10 +02:00
|
|
|
private validateAdminToken({ type, project, environment }) {
|
|
|
|
if (type === ApiTokenType.ADMIN && project !== ALL) {
|
|
|
|
throw new BadDataError(
|
|
|
|
'Admin token cannot be scoped to single project',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === ApiTokenType.ADMIN && environment !== ALL) {
|
|
|
|
throw new BadDataError(
|
|
|
|
'Admin token cannot be scoped to single environment',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async createApiToken(
|
|
|
|
newToken: Omit<IApiTokenCreate, 'secret'>,
|
2021-03-29 19:58:11 +02:00
|
|
|
): Promise<IApiToken> {
|
2021-09-15 20:28:10 +02:00
|
|
|
this.validateAdminToken(newToken);
|
|
|
|
|
|
|
|
const secret = this.generateSecretKey(newToken);
|
|
|
|
const createNewToken = { ...newToken, secret };
|
|
|
|
|
|
|
|
try {
|
|
|
|
const token = await this.store.insert(createNewToken);
|
|
|
|
this.activeTokens.push(token);
|
|
|
|
return token;
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code === FOREIGN_KEY_VIOLATION) {
|
|
|
|
let { message } = error;
|
|
|
|
if (error.constraint === 'api_tokens_project_fkey') {
|
|
|
|
message = `Project=${newToken.project} does not exist`;
|
|
|
|
} else if (error.constraint === 'api_tokens_environment_fkey') {
|
|
|
|
message = `Environment=${newToken.environment} does not exist`;
|
|
|
|
}
|
|
|
|
throw new BadDataError(message);
|
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
}
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
2021-09-15 20:28:10 +02:00
|
|
|
private generateSecretKey({ project, environment }) {
|
|
|
|
const randomStr = crypto.randomBytes(28).toString('hex');
|
|
|
|
return `${project}:${environment}.${randomStr}`;
|
2021-03-29 19:58:11 +02:00
|
|
|
}
|
|
|
|
|
2021-04-22 23:40:52 +02:00
|
|
|
destroy(): void {
|
2021-03-29 19:58:11 +02:00
|
|
|
clearInterval(this.timer);
|
|
|
|
this.timer = null;
|
|
|
|
}
|
|
|
|
}
|