import crypto from 'crypto'; import { Logger } from '../logger'; import { ADMIN, CLIENT } from '../types/permissions'; import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; import ApiUser from '../types/api-user'; import { ApiTokenType, IApiToken, IApiTokenCreate, validateApiToken, validateApiTokenEnvironment, } 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'; import { minutesToMilliseconds } from 'date-fns'; import { IEnvironmentStore } from 'lib/types/stores/environment-store'; export class ApiTokenService { private store: IApiTokenStore; private environmentStore: IEnvironmentStore; private logger: Logger; private timer: NodeJS.Timeout; private activeTokens: IApiToken[] = []; constructor( { apiTokenStore, environmentStore, }: Pick, config: Pick, ) { this.store = apiTokenStore; this.environmentStore = environmentStore; this.logger = config.getLogger('/services/api-token-service.ts'); this.fetchActiveTokens(); this.timer = setInterval( () => this.fetchActiveTokens(), minutesToMilliseconds(1), ).unref(); if (config.authentication.initApiTokens.length > 0) { process.nextTick(async () => this.initApiTokens(config.authentication.initApiTokens), ); } } private async fetchActiveTokens(): Promise { try { this.activeTokens = await this.getAllActiveTokens(); } finally { // eslint-disable-next-line no-unsafe-finally return; } } public async getAllTokens(): Promise { return this.store.getAll(); } public async getAllActiveTokens(): Promise { return this.store.getAllActive(); } private async initApiTokens(tokens: IApiTokenCreate[]) { const tokenCount = await this.store.count(); if (tokenCount > 0) { return; } try { const createAll = tokens.map((t) => this.insertNewApiToken(t)); await Promise.all(createAll); } catch (e) { this.logger.error('Unable to create initial Admin API tokens'); } } public getUserForToken(secret: string): ApiUser | undefined { const token = this.activeTokens.find((t) => t.secret === secret); if (token) { const permissions = token.type === ApiTokenType.ADMIN ? [ADMIN] : [CLIENT]; return new ApiUser({ username: token.username, permissions, project: token.project, environment: token.environment, type: token.type, }); } return undefined; } public async updateExpiry( secret: string, expiresAt: Date, ): Promise { return this.store.setExpiry(secret, expiresAt); } public async delete(secret: string): Promise { return this.store.delete(secret); } public async createApiToken( newToken: Omit, ): Promise { validateApiToken(newToken); const environments = await this.environmentStore.getAll(); validateApiTokenEnvironment(newToken, environments); const secret = this.generateSecretKey(newToken); const createNewToken = { ...newToken, secret }; return this.insertNewApiToken(createNewToken); } private async insertNewApiToken( newApiToken: IApiTokenCreate, ): Promise { try { const token = await this.store.insert(newApiToken); 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=${newApiToken.project} does not exist`; } else if (error.constraint === 'api_tokens_environment_fkey') { message = `Environment=${newApiToken.environment} does not exist`; } throw new BadDataError(message); } throw error; } } private generateSecretKey({ project, environment }) { const randomStr = crypto.randomBytes(28).toString('hex'); return `${project}:${environment}.${randomStr}`; } destroy(): void { clearInterval(this.timer); this.timer = null; } }