unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
269 lines • 11.6 kB
JavaScript
import Controller from '../controller.js';
import { ADMIN, CREATE_CLIENT_API_TOKEN, CREATE_FRONTEND_API_TOKEN, DELETE_CLIENT_API_TOKEN, DELETE_FRONTEND_API_TOKEN, READ_CLIENT_API_TOKEN, READ_FRONTEND_API_TOKEN, UPDATE_CLIENT_API_TOKEN, UPDATE_FRONTEND_API_TOKEN, } from '../../types/permissions.js';
import { ApiTokenType } from '../../types/model.js';
import { createApiToken } from '../../schema/api-token-schema.js';
import { createRequestSchema } from '../../openapi/util/create-request-schema.js';
import { createResponseSchema, resourceCreatedResponseSchema, } from '../../openapi/util/create-response-schema.js';
import { apiTokensSchema, } from '../../openapi/spec/api-tokens-schema.js';
import { serializeDates } from '../../types/serialize-dates.js';
import { apiTokenSchema, } from '../../openapi/spec/api-token-schema.js';
import { emptyResponse, getStandardResponses, } from '../../openapi/util/standard-responses.js';
import { OperationDeniedError } from '../../error/index.js';
export const tokenTypeToCreatePermission = (tokenType) => {
switch (tokenType) {
case ApiTokenType.ADMIN:
return ADMIN;
case ApiTokenType.CLIENT:
return CREATE_CLIENT_API_TOKEN;
case ApiTokenType.FRONTEND:
return CREATE_FRONTEND_API_TOKEN;
}
};
const permissionToTokenType = (permission) => {
if ([
CREATE_FRONTEND_API_TOKEN,
READ_FRONTEND_API_TOKEN,
DELETE_FRONTEND_API_TOKEN,
UPDATE_FRONTEND_API_TOKEN,
].includes(permission)) {
return ApiTokenType.FRONTEND;
}
else if ([
CREATE_CLIENT_API_TOKEN,
READ_CLIENT_API_TOKEN,
DELETE_CLIENT_API_TOKEN,
UPDATE_CLIENT_API_TOKEN,
].includes(permission)) {
return ApiTokenType.CLIENT;
}
else if (ADMIN === permission) {
return ApiTokenType.ADMIN;
}
else {
return undefined;
}
};
const tokenTypeToUpdatePermission = (tokenType) => {
switch (tokenType) {
case ApiTokenType.ADMIN:
return ADMIN;
case ApiTokenType.CLIENT:
return UPDATE_CLIENT_API_TOKEN;
case ApiTokenType.FRONTEND:
return UPDATE_FRONTEND_API_TOKEN;
}
};
const tokenTypeToDeletePermission = (tokenType) => {
switch (tokenType) {
case ApiTokenType.ADMIN:
return ADMIN;
case ApiTokenType.CLIENT:
return DELETE_CLIENT_API_TOKEN;
case ApiTokenType.FRONTEND:
return DELETE_FRONTEND_API_TOKEN;
}
};
export class ApiTokenController extends Controller {
constructor(config, { apiTokenService, accessService, frontendApiService, openApiService, }) {
super(config);
this.apiTokenService = apiTokenService;
this.accessService = accessService;
this.frontendApiService = frontendApiService;
this.openApiService = openApiService;
this.flagResolver = config.flagResolver;
this.logger = config.getLogger('api-token-controller.js');
this.route({
method: 'get',
path: '',
handler: this.getAllApiTokens,
permission: [ADMIN, READ_CLIENT_API_TOKEN, READ_FRONTEND_API_TOKEN],
middleware: [
openApiService.validPath({
tags: ['API tokens'],
operationId: 'getAllApiTokens',
summary: 'Get API tokens',
description: 'Retrieves all API tokens that exist in the Unleash instance.',
responses: {
200: createResponseSchema('apiTokensSchema'),
...getStandardResponses(401, 403),
},
}),
],
});
this.route({
method: 'get',
path: '/:name',
handler: this.getApiTokensByName,
permission: [ADMIN, READ_CLIENT_API_TOKEN, READ_FRONTEND_API_TOKEN],
middleware: [
openApiService.validPath({
tags: ['API tokens'],
operationId: 'getApiTokensByName',
summary: 'Get API tokens by name',
description: 'Retrieves all API tokens that match a given token name. Because token names are not unique, this endpoint will always return a list. If no tokens with the provided name exist, the list will be empty. Otherwise, it will contain all the tokens with the given name.',
responses: {
200: createResponseSchema('apiTokensSchema'),
...getStandardResponses(401, 403),
},
}),
],
});
this.route({
method: 'post',
path: '',
handler: this.createApiToken,
permission: [
ADMIN,
CREATE_CLIENT_API_TOKEN,
CREATE_FRONTEND_API_TOKEN,
],
middleware: [
openApiService.validPath({
tags: ['API tokens'],
operationId: 'createApiToken',
requestBody: createRequestSchema('createApiTokenSchema'),
summary: 'Create API token',
description: `Create an API token of a specific type: one of ${Object.values(ApiTokenType).join(', ')}.`,
responses: {
201: resourceCreatedResponseSchema('apiTokenSchema'),
...getStandardResponses(401, 403, 415),
},
}),
],
});
this.route({
method: 'put',
path: '/:token',
handler: this.updateApiToken,
permission: [
ADMIN,
UPDATE_CLIENT_API_TOKEN,
UPDATE_FRONTEND_API_TOKEN,
],
middleware: [
openApiService.validPath({
tags: ['API tokens'],
operationId: 'updateApiToken',
summary: 'Update API token',
description: "Updates an existing API token with a new expiry date. The `token` path parameter is the token's `secret`. If the token does not exist, this endpoint returns a 200 OK, but does nothing.",
requestBody: createRequestSchema('updateApiTokenSchema'),
responses: {
200: emptyResponse,
...getStandardResponses(401, 403, 415),
},
}),
],
});
this.route({
method: 'delete',
path: '/:token',
handler: this.deleteApiToken,
acceptAnyContentType: true,
permission: [
ADMIN,
DELETE_CLIENT_API_TOKEN,
DELETE_FRONTEND_API_TOKEN,
],
middleware: [
openApiService.validPath({
tags: ['API tokens'],
summary: 'Delete API token',
description: "Deletes an existing API token. The `token` path parameter is the token's `secret`. If the token does not exist, this endpoint returns a 200 OK, but does nothing.",
operationId: 'deleteApiToken',
responses: {
200: emptyResponse,
...getStandardResponses(401, 403),
},
}),
],
});
}
async getAllApiTokens(req, res) {
const { user } = req;
const tokens = await this.accessibleTokens(user);
this.openApiService.respondWithValidation(200, res, apiTokensSchema.$id, { tokens: serializeDates(tokens) });
}
async getApiTokensByName(req, res) {
const { user } = req;
const { name } = req.params;
const tokens = await this.accessibleTokensByName(name, user);
this.openApiService.respondWithValidation(200, res, apiTokensSchema.$id, { tokens: serializeDates(tokens) });
}
async createApiToken(req, res) {
const createToken = await createApiToken.validateAsync(req.body);
const permissionRequired = tokenTypeToCreatePermission(createToken.type);
const hasPermission = await this.accessService.hasPermission(req.user, permissionRequired);
if (hasPermission) {
const token = await this.apiTokenService.createApiTokenWithProjects(createToken, req.audit);
this.openApiService.respondWithValidation(201, res, apiTokenSchema.$id, serializeDates(token), { location: `api-tokens` });
}
else {
throw new OperationDeniedError(`You don't have the necessary access [${permissionRequired}] to perform this operation`);
}
}
async updateApiToken(req, res) {
const { token } = req.params;
const { expiresAt } = req.body;
if (!expiresAt) {
this.logger.error(req.body);
return res.status(400).send();
}
let tokenToUpdate;
try {
tokenToUpdate = await this.apiTokenService.getToken(token);
}
catch (error) { }
if (!tokenToUpdate) {
res.status(200).end();
return;
}
const permissionRequired = tokenTypeToUpdatePermission(tokenToUpdate.type);
const hasPermission = await this.accessService.hasPermission(req.user, permissionRequired);
if (!hasPermission) {
throw new OperationDeniedError(`You do not have the required access [${permissionRequired}] to perform this operation`);
}
await this.apiTokenService.updateExpiry(token, new Date(expiresAt), req.audit);
return res.status(200).end();
}
async deleteApiToken(req, res) {
const { token } = req.params;
let tokenToUpdate;
try {
tokenToUpdate = await this.apiTokenService.getToken(token);
}
catch (error) { }
if (!tokenToUpdate) {
res.status(200).end();
return;
}
const permissionRequired = tokenTypeToDeletePermission(tokenToUpdate.type);
const hasPermission = await this.accessService.hasPermission(req.user, permissionRequired);
if (!hasPermission) {
throw new OperationDeniedError(`You do not have the required access [${permissionRequired}] to perform this operation`);
}
await this.apiTokenService.delete(token, req.audit);
await this.frontendApiService.deleteClientForFrontendApiToken(token);
res.status(200).end();
}
async accessibleTokensByName(tokenName, user) {
const allTokens = await this.accessibleTokens(user);
return allTokens.filter((token) => token.tokenName === tokenName);
}
async accessibleTokens(user) {
const allTokens = await this.apiTokenService.getAllTokens();
if (user.isAPI && user.permissions.includes(ADMIN)) {
return allTokens;
}
const userPermissions = await this.accessService.getPermissionsForUser(user);
const allowedTokenTypes = [
ADMIN,
READ_CLIENT_API_TOKEN,
READ_FRONTEND_API_TOKEN,
]
.filter((readPerm) => userPermissions.some((p) => p.permission === readPerm || p.permission === ADMIN))
.map(permissionToTokenType)
.filter((t) => t);
return allTokens.filter((token) => allowedTokenTypes.includes(token.type));
}
}
//# sourceMappingURL=api-token.js.map