diff --git a/src/lib/routes/admin-api/api-token.ts b/src/lib/routes/admin-api/api-token.ts index ad282b6660..b12a651523 100644 --- a/src/lib/routes/admin-api/api-token.ts +++ b/src/lib/routes/admin-api/api-token.ts @@ -3,10 +3,22 @@ import { Response } from 'express'; import Controller from '../controller'; import { ADMIN, + CREATE_ADMIN_API_TOKEN, CREATE_API_TOKEN, + CREATE_CLIENT_API_TOKEN, + CREATE_FRONTEND_API_TOKEN, + DELETE_ADMIN_API_TOKEN, DELETE_API_TOKEN, + DELETE_CLIENT_API_TOKEN, + DELETE_FRONTEND_API_TOKEN, + READ_ADMIN_API_TOKEN, READ_API_TOKEN, + READ_CLIENT_API_TOKEN, + READ_FRONTEND_API_TOKEN, + UPDATE_ADMIN_API_TOKEN, UPDATE_API_TOKEN, + UPDATE_CLIENT_API_TOKEN, + UPDATE_FRONTEND_API_TOKEN, } from '../../types/permissions'; import { ApiTokenService } from '../../services/api-token-service'; import { Logger } from '../../logger'; @@ -36,10 +48,85 @@ import { UpdateApiTokenSchema } from '../../openapi/spec/update-api-token-schema import { emptyResponse } from '../../openapi/util/standard-responses'; import { ProxyService } from '../../services/proxy-service'; import { extractUsername } from '../../util'; +import { OperationDeniedError } from '../../error'; interface TokenParam { token: string; } +const tokenTypeToCreatePermission: (tokenType: ApiTokenType) => string = ( + tokenType, +) => { + switch (tokenType) { + case ApiTokenType.ADMIN: + return CREATE_ADMIN_API_TOKEN; + case ApiTokenType.CLIENT: + return CREATE_CLIENT_API_TOKEN; + case ApiTokenType.FRONTEND: + return CREATE_FRONTEND_API_TOKEN; + } +}; + +const permissionToTokenType: ( + permission: string, +) => ApiTokenType | undefined = (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 ( + [ + READ_ADMIN_API_TOKEN, + CREATE_ADMIN_API_TOKEN, + DELETE_ADMIN_API_TOKEN, + UPDATE_ADMIN_API_TOKEN, + ].includes(permission) + ) { + return ApiTokenType.ADMIN; + } else { + return undefined; + } +}; + +const tokenTypeToUpdatePermission: (tokenType: ApiTokenType) => string = ( + tokenType, +) => { + switch (tokenType) { + case ApiTokenType.ADMIN: + return UPDATE_ADMIN_API_TOKEN; + case ApiTokenType.CLIENT: + return UPDATE_CLIENT_API_TOKEN; + case ApiTokenType.FRONTEND: + return UPDATE_FRONTEND_API_TOKEN; + } +}; + +const tokenTypeToDeletePermission: (tokenType: ApiTokenType) => string = ( + tokenType, +) => { + switch (tokenType) { + case ApiTokenType.ADMIN: + return DELETE_ADMIN_API_TOKEN; + case ApiTokenType.CLIENT: + return DELETE_CLIENT_API_TOKEN; + case ApiTokenType.FRONTEND: + return DELETE_FRONTEND_API_TOKEN; + } +}; + export class ApiTokenController extends Controller { private apiTokenService: ApiTokenService; @@ -160,17 +247,30 @@ export class ApiTokenController extends Controller { res: Response, ): Promise { const createToken = await createApiToken.validateAsync(req.body); - const token = await this.apiTokenService.createApiToken( - createToken, - extractUsername(req), + const permissionRequired = tokenTypeToCreatePermission( + createToken.type, ); - this.openApiService.respondWithValidation( - 201, - res, - apiTokenSchema.$id, - serializeDates(token), - { location: `api-tokens` }, + const hasPermission = await this.accessService.hasPermission( + req.user, + permissionRequired, ); + if (hasPermission) { + const token = await this.apiTokenService.createApiToken( + createToken, + extractUsername(req), + ); + 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( @@ -184,12 +284,33 @@ export class ApiTokenController extends Controller { 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), extractUsername(req), ); + return res.status(200).end(); } @@ -198,7 +319,26 @@ export class ApiTokenController extends Controller { res: Response, ): Promise { 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, + ); + let 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, extractUsername(req)); await this.proxyService.deleteClientForProxyToken(token); res.status(200).end(); @@ -210,11 +350,21 @@ export class ApiTokenController extends Controller { if (user.isAPI && user.permissions.includes(ADMIN)) { return allTokens; } - - if (await this.accessService.hasPermission(user, UPDATE_API_TOKEN)) { - return allTokens; - } - - return allTokens.filter((token) => token.type !== ApiTokenType.ADMIN); + const userPermissions = await this.accessService.getPermissionsForUser( + user, + ); + let allowedTokenTypes = [ + READ_ADMIN_API_TOKEN, + READ_CLIENT_API_TOKEN, + READ_FRONTEND_API_TOKEN, + ] + .filter((readPerm) => + userPermissions.some((p) => p.permission === readPerm), + ) + .map(permissionToTokenType) + .filter((t) => t); + return allTokens.filter((token) => + allowedTokenTypes.includes(token.type), + ); } } diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index cb01e84434..4585337983 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -130,7 +130,6 @@ export class AccessService { try { const userP = await this.getPermissionsForUser(user); - return userP .filter( (p) => diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index d78e8681c1..24b050b490 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -88,6 +88,10 @@ export class ApiTokenService { } } + async getToken(secret: string): Promise { + return this.store.get(secret); + } + async updateLastSeen(): Promise { if (this.lastSeenSecrets.size > 0) { const toStore = [...this.lastSeenSecrets]; diff --git a/src/lib/types/permissions.ts b/src/lib/types/permissions.ts index 412648dd1f..fff6de2edc 100644 --- a/src/lib/types/permissions.ts +++ b/src/lib/types/permissions.ts @@ -30,6 +30,22 @@ export const UPDATE_API_TOKEN = 'UPDATE_API_TOKEN'; export const CREATE_API_TOKEN = 'CREATE_API_TOKEN'; export const DELETE_API_TOKEN = 'DELETE_API_TOKEN'; export const READ_API_TOKEN = 'READ_API_TOKEN'; + +export const UPDATE_ADMIN_API_TOKEN = 'UPDATE_ADMIN_API_TOKEN'; +export const CREATE_ADMIN_API_TOKEN = 'CREATE_ADMIN_API_TOKEN'; +export const DELETE_ADMIN_API_TOKEN = 'DELETE_ADMIN_API_TOKEN'; +export const READ_ADMIN_API_TOKEN = 'READ_ADMIN_API_TOKEN'; + +export const UPDATE_CLIENT_API_TOKEN = 'UPDATE_CLIENT_API_TOKEN'; +export const CREATE_CLIENT_API_TOKEN = 'CREATE_CLIENT_API_TOKEN'; +export const DELETE_CLIENT_API_TOKEN = 'DELETE_CLIENT_API_TOKEN'; +export const READ_CLIENT_API_TOKEN = 'READ_CLIENT_API_TOKEN'; + +export const UPDATE_FRONTEND_API_TOKEN = 'UPDATE_FRONTEND_API_TOKEN'; +export const CREATE_FRONTEND_API_TOKEN = 'CREATE_FRONTEND_API_TOKEN'; +export const DELETE_FRONTEND_API_TOKEN = 'DELETE_FRONTEND_API_TOKEN'; +export const READ_FRONTEND_API_TOKEN = 'READ_FRONTEND_API_TOKEN'; + export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE'; export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE'; export const UPDATE_FEATURE_VARIANTS = 'UPDATE_FEATURE_VARIANTS'; diff --git a/src/migrations/20230619105029-new-fine-grained-api-token-permissions.js b/src/migrations/20230619105029-new-fine-grained-api-token-permissions.js new file mode 100644 index 0000000000..dd9092b26a --- /dev/null +++ b/src/migrations/20230619105029-new-fine-grained-api-token-permissions.js @@ -0,0 +1,31 @@ +exports.up = function (db, cb) { + db.runSql( + ` + INSERT INTO permissions(permission, display_name, type) VALUES + ('CREATE_ADMIN_API_TOKEN', 'Allowed to create new ADMIN tokens', 'root'), + ('UPDATE_ADMIN_API_TOKEN', 'Allowed to update ADMIN tokens', 'root'), + ('DELETE_ADMIN_API_TOKEN', 'Allowed to delete ADMIN tokens', 'root'), + ('READ_ADMIN_API_TOKEN', 'Allowed to read ADMIN tokens', 'root'), + ('CREATE_CLIENT_API_TOKEN', 'Allowed to create new CLIENT tokens', 'root'), + ('UPDATE_CLIENT_API_TOKEN', 'Allowed to update CLIENT tokens', 'root'), + ('DELETE_CLIENT_API_TOKEN', 'Allowed to delete CLIENT tokens', 'root'), + ('READ_CLIENT_API_TOKEN', 'Allowed to read CLIENT tokens', 'root'), + ('CREATE_FRONTEND_API_TOKEN', 'Allowed to create new FRONTEND tokens', 'root'), + ('UPDATE_FRONTEND_API_TOKEN', 'Allowed to update FRONTEND tokens', 'root'), + ('DELETE_FRONTEND_API_TOKEN', 'Allowed to delete FRONTEND tokens', 'root'), + ('READ_FRONTEND_API_TOKEN', 'Allowed to read FRONTEND tokens', 'root'); + `, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + DELETE FROM permissions WHERE permission IN ('CREATE_ADMIN_API_TOKEN', 'UPDATE_ADMIN_API_TOKEN', 'DELETE_ADMIN_API_TOKEN', 'READ_ADMIN_API_TOKEN'); + DELETE FROM permissions WHERE permission IN ('CREATE_CLIENT_API_TOKEN', 'UPDATE_CLIENT_API_TOKEN', 'DELETE_CLIENT_API_TOKEN', 'READ_CLIENT_API_TOKEN'); + DELETE FROM permissions WHERE permission IN ('CREATE_FRONTEND_API_TOKEN', 'UPDATE_FRONTEND_API_TOKEN', 'DELETE_FRONTEND_API_TOKEN', 'READ_FRONTEND_API_TOKEN'); + `, + cb, + ); +}; diff --git a/src/migrations/20230619110243-assign-apitoken-permissions-to-rootroles.js b/src/migrations/20230619110243-assign-apitoken-permissions-to-rootroles.js new file mode 100644 index 0000000000..4400e953c8 --- /dev/null +++ b/src/migrations/20230619110243-assign-apitoken-permissions-to-rootroles.js @@ -0,0 +1,27 @@ +exports.up = function (db, cb) { + db.runSql( + ` + CREATE OR REPLACE FUNCTION assign_unleash_permission_to_role(permission_name text, role_name text) returns void as + $$ + declare role_id int; + permission_id int; +BEGIN + role_id := (SELECT id FROM roles WHERE name = role_name); + permission_id := (SELECT p.id FROM permissions p WHERE p.permission = permission_name); + INSERT INTO role_permission(role_id, permission_id) VALUES (role_id, permission_id); +END +$$ language plpgsql; + + SELECT assign_unleash_permission_to_role('READ_CLIENT_API_TOKEN', 'Editor'); + SELECT assign_unleash_permission_to_role('READ_FRONTEND_API_TOKEN', 'Editor'); + + `, + cb, + ); +}; +exports.down = function (db, cb) { + db.runSql( + `DROP FUNCTION IF EXISTS assign_unleash_permission_to_role(text, text)`, + cb, + ); +}; diff --git a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts index 0c1803e019..0a51f92dfe 100644 --- a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts @@ -3,6 +3,19 @@ import dbInit from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { ApiTokenType } from '../../../../lib/types/models/api-token'; import { RoleName } from '../../../../lib/types/model'; +import { + CREATE_API_TOKEN, + CREATE_CLIENT_API_TOKEN, + DELETE_API_TOKEN, + DELETE_CLIENT_API_TOKEN, + READ_ADMIN_API_TOKEN, + READ_API_TOKEN, + READ_CLIENT_API_TOKEN, + READ_FRONTEND_API_TOKEN, + UPDATE_API_TOKEN, + UPDATE_CLIENT_API_TOKEN, +} from '../../../../lib/types'; +import { addDays } from 'date-fns'; let stores; let db; @@ -22,8 +35,8 @@ afterEach(async () => { await stores.apiTokenStore.deleteAll(); }); -test('editor users should only get client tokens', async () => { - expect.assertions(2); +test('editor users should only get client or frontend tokens', async () => { + expect.assertions(3); const preHook = (app, config, { userService, accessService }) => { app.use('/api/admin/', async (req, res, next) => { @@ -45,6 +58,12 @@ test('editor users should only get client tokens', async () => { type: ApiTokenType.CLIENT, }); + await stores.apiTokenStore.insert({ + username: 'frontend', + secret: '12345', + type: ApiTokenType.FRONTEND, + }); + await stores.apiTokenStore.insert({ username: 'test', secret: 'sdfsdf2d', @@ -56,8 +75,9 @@ test('editor users should only get client tokens', async () => { .expect('Content-Type', /json/) .expect(200) .expect((res) => { - expect(res.body.tokens.length).toBe(1); + expect(res.body.tokens.length).toBe(2); expect(res.body.tokens[0].type).toBe(ApiTokenType.CLIENT); + expect(res.body.tokens[1].type).toBe(ApiTokenType.FRONTEND); }); await destroy(); @@ -155,3 +175,759 @@ test('Token-admin should be allowed to create token', async () => { await destroy(); }); + +describe('Fine grained API token permissions', () => { + describe('A role with access to CREATE_CLIENT_API_TOKEN', () => { + test('should be allowed to create client tokens', async () => { + const preHook = (app, config, { userService, accessService }) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'mylittlepony_viewer@example.com', + rootRole: role.id, + }); + req.user = user; + const createClientApiTokenRole = + await accessService.createRole({ + name: 'client_token_creator', + description: 'Can create client tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + role.id, + CREATE_API_TOKEN, + ); + await accessService.addPermissionToRole( + role.id, + CREATE_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + createClientApiTokenRole.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + await request + .post('/api/admin/api-tokens') + .send({ + username: 'default-client', + type: 'client', + }) + .set('Content-Type', 'application/json') + .expect(201); + await destroy(); + }); + test('should NOT be allowed to create frontend tokens', async () => { + const preHook = (app, config, { userService, accessService }) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'mylittlepony_viewer_frontend@example.com', + rootRole: role.id, + }); + req.user = user; + const createClientApiTokenRole = + await accessService.createRole({ + name: 'client_token_creator_cannot_create_frontend', + description: 'Can create client tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + role.id, + CREATE_API_TOKEN, + ); + await accessService.addPermissionToRole( + role.id, + CREATE_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + createClientApiTokenRole.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + await request + .post('/api/admin/api-tokens') + .send({ + username: 'default-frontend', + type: 'frontend', + }) + .set('Content-Type', 'application/json') + .expect(403); + await destroy(); + }); + test('should NOT be allowed to create ADMIN tokens', async () => { + const preHook = (app, config, { userService, accessService }) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'mylittlepony_admin@example.com', + rootRole: role.id, + }); + req.user = user; + const createClientApiTokenRole = + await accessService.createRole({ + name: 'client_token_creator_cannot_create_admin', + description: 'Can create client tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + role.id, + CREATE_API_TOKEN, + ); + await accessService.addPermissionToRole( + role.id, + CREATE_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + createClientApiTokenRole.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + await request + .post('/api/admin/api-tokens') + .send({ + username: 'default-admin', + type: 'admin', + }) + .set('Content-Type', 'application/json') + .expect(403); + await destroy(); + }); + }); + describe('Read operations', () => { + test('READ_FRONTEND_API_TOKEN should be able to see FRONTEND tokens', async () => { + const preHook = (app, config, { userService, accessService }) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'read_frontend_token@example.com', + rootRole: role.id, + }); + req.user = user; + const readFrontendApiToken = await accessService.createRole( + { + name: 'frontend_token_reader', + description: 'Can read frontend tokens', + permissions: [], + type: 'root-custom', + }, + ); + await accessService.addPermissionToRole( + readFrontendApiToken.id, + READ_API_TOKEN, + ); + await accessService.addPermissionToRole( + readFrontendApiToken.id, + READ_FRONTEND_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + readFrontendApiToken.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + await stores.apiTokenStore.insert({ + username: 'client', + secret: 'client_secret', + type: ApiTokenType.CLIENT, + }); + + await stores.apiTokenStore.insert({ + username: 'admin', + secret: 'sdfsdf2admin_secret', + type: ApiTokenType.ADMIN, + }); + await stores.apiTokenStore.insert({ + username: 'frontender', + secret: 'sdfsdf2dfrontend_Secret', + type: ApiTokenType.FRONTEND, + }); + await request + .get('/api/admin/api-tokens') + .set('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect( + res.body.tokens.every( + (t) => t.type === ApiTokenType.FRONTEND, + ), + ).toBe(true); + }); + await destroy(); + }); + test('READ_CLIENT_API_TOKEN should be able to see CLIENT tokens', async () => { + const preHook = (app, config, { userService, accessService }) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'read_client_token@example.com', + rootRole: role.id, + }); + req.user = user; + const readClientTokenRole = await accessService.createRole({ + name: 'client_token_reader', + description: 'Can read client tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + readClientTokenRole.id, + READ_API_TOKEN, + ); + await accessService.addPermissionToRole( + readClientTokenRole.id, + READ_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + readClientTokenRole.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + await stores.apiTokenStore.insert({ + username: 'client', + secret: 'client_secret_1234', + type: ApiTokenType.CLIENT, + }); + + await stores.apiTokenStore.insert({ + username: 'admin', + secret: 'admin_secret_1234', + type: ApiTokenType.ADMIN, + }); + await stores.apiTokenStore.insert({ + username: 'frontender', + secret: 'frontend_secret_1234', + type: ApiTokenType.FRONTEND, + }); + await request + .get('/api/admin/api-tokens') + .set('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body.tokens).toHaveLength(1); + expect(res.body.tokens[0].type).toBe(ApiTokenType.CLIENT); + }); + await destroy(); + }); + test('READ_ADMIN_API_TOKEN should be able to see ADMIN tokens', async () => { + const preHook = (app, config, { userService, accessService }) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'read_admin_token@example.com', + rootRole: role.id, + }); + req.user = user; + const readAdminApiToken = await accessService.createRole({ + name: 'admin_token_reader', + description: 'Can read admin tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + readAdminApiToken.id, + READ_API_TOKEN, + ); + await accessService.addPermissionToRole( + readAdminApiToken.id, + READ_ADMIN_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + readAdminApiToken.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + await stores.apiTokenStore.insert({ + username: 'client', + secret: 'client_secret_4321', + type: ApiTokenType.CLIENT, + }); + + await stores.apiTokenStore.insert({ + username: 'admin', + secret: 'admin_secret_4321', + type: ApiTokenType.ADMIN, + }); + await stores.apiTokenStore.insert({ + username: 'frontender', + secret: 'frontend_secret_4321', + type: ApiTokenType.FRONTEND, + }); + await request + .get('/api/admin/api-tokens') + .set('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body.tokens).toHaveLength(1); + expect(res.body.tokens[0].type).toBe(ApiTokenType.ADMIN); + }); + await destroy(); + }); + }); + describe('Update operations', () => { + describe('UPDATE_CLIENT_API_TOKEN can', () => { + test('UPDATE client_api token expiry', async () => { + const preHook = ( + app, + config, + { userService, accessService }, + ) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'update_client_token@example.com', + rootRole: role.id, + }); + req.user = user; + const updateClientApiExpiry = + await accessService.createRole({ + name: 'update_client_token', + description: 'Can update client tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + UPDATE_API_TOKEN, + ); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + UPDATE_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + updateClientApiExpiry.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + const token = await stores.apiTokenStore.insert({ + username: 'cilent', + secret: 'update_client_token', + type: ApiTokenType.CLIENT, + }); + await request + .put(`/api/admin/api-tokens/${token.secret}`) + .send({ expiresAt: addDays(new Date(), 14) }) + .expect(200); + await destroy(); + }); + test('NOT UPDATE frontend_api token expiry', async () => { + const preHook = ( + app, + config, + { userService, accessService }, + ) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'update_frontend_token@example.com', + rootRole: role.id, + }); + req.user = user; + const updateClientApiExpiry = + await accessService.createRole({ + name: 'update_client_token_not_frontend', + description: 'Can not update frontend tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + UPDATE_API_TOKEN, + ); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + UPDATE_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + updateClientApiExpiry.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + const token = await stores.apiTokenStore.insert({ + username: 'frontend', + secret: 'update_frontend_token', + type: ApiTokenType.FRONTEND, + }); + await request + .put(`/api/admin/api-tokens/${token.secret}`) + .send({ expiresAt: addDays(new Date(), 14) }) + .expect(403); + + await destroy(); + }); + test('NOT UPDATE admin_api token expiry', async () => { + const preHook = ( + app, + config, + { userService, accessService }, + ) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'update_admin_token@example.com', + rootRole: role.id, + }); + req.user = user; + const updateClientApiExpiry = + await accessService.createRole({ + name: 'update_client_token_not_admin', + description: 'Can not update admin tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + UPDATE_API_TOKEN, + ); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + UPDATE_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + updateClientApiExpiry.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + const token = await stores.apiTokenStore.insert({ + username: 'admin', + secret: 'update_admin_token', + type: ApiTokenType.ADMIN, + }); + await request + .put(`/api/admin/api-tokens/${token.secret}`) + .send({ expiresAt: addDays(new Date(), 14) }) + .expect(403); + await destroy(); + }); + }); + }); + describe('Delete operations', () => { + describe('DELETE_CLIENT_API_TOKEN can', () => { + test('DELETE client_api token', async () => { + const preHook = ( + app, + config, + { userService, accessService }, + ) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'delete_client_token@example.com', + rootRole: role.id, + }); + req.user = user; + const updateClientApiExpiry = + await accessService.createRole({ + name: 'delete_client_token', + description: 'Can delete client tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + DELETE_API_TOKEN, + ); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + DELETE_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + updateClientApiExpiry.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + const token = await stores.apiTokenStore.insert({ + username: 'cilent', + secret: 'delete_client_token', + type: ApiTokenType.CLIENT, + }); + await request + .delete(`/api/admin/api-tokens/${token.secret}`) + .send({ expiresAt: addDays(new Date(), 14) }) + .expect(200); + await destroy(); + }); + test('NOT DELETE frontend_api token', async () => { + const preHook = ( + app, + config, + { userService, accessService }, + ) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'delete_frontend_token@example.com', + rootRole: role.id, + }); + req.user = user; + const updateClientApiExpiry = + await accessService.createRole({ + name: 'delete_client_token_not_frontend', + description: 'Can not delete frontend tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + DELETE_API_TOKEN, + ); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + DELETE_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + updateClientApiExpiry.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + const token = await stores.apiTokenStore.insert({ + username: 'frontend', + secret: 'delete_frontend_token', + type: ApiTokenType.FRONTEND, + }); + await request + .delete(`/api/admin/api-tokens/${token.secret}`) + .send({ expiresAt: addDays(new Date(), 14) }) + .expect(403); + await destroy(); + }); + test('NOT DELETE admin_api token', async () => { + const preHook = ( + app, + config, + { userService, accessService }, + ) => { + app.use('/api/admin/', async (req, res, next) => { + const role = await accessService.getRootRole( + RoleName.VIEWER, + ); + const user = await userService.createUser({ + email: 'delete_admin_token@example.com', + rootRole: role.id, + }); + req.user = user; + const updateClientApiExpiry = + await accessService.createRole({ + name: 'delete_client_token_not_admin', + description: 'Can not delete admin tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + DELETE_API_TOKEN, + ); + await accessService.addPermissionToRole( + updateClientApiExpiry.id, + DELETE_CLIENT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + updateClientApiExpiry.id, + 'default', + ); + next(); + }); + }; + const { request, destroy } = await setupAppWithCustomAuth( + stores, + preHook, + { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }, + ); + const token = await stores.apiTokenStore.insert({ + username: 'admin', + secret: 'delete_admin_token', + type: ApiTokenType.ADMIN, + }); + await request + .delete(`/api/admin/api-tokens/${token.secret}`) + .send({ expiresAt: addDays(new Date(), 14) }) + .expect(403); + await destroy(); + }); + }); + }); +}); diff --git a/src/test/e2e/api/admin/api-token.e2e.test.ts b/src/test/e2e/api/admin/api-token.e2e.test.ts index 4adfa8d887..0377a2997c 100644 --- a/src/test/e2e/api/admin/api-token.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.e2e.test.ts @@ -3,6 +3,7 @@ import dbInit from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { ALL, ApiTokenType } from '../../../../lib/types/models/api-token'; import { DEFAULT_ENV } from '../../../../lib/util'; +import { addDays } from 'date-fns'; let db; let app; @@ -107,7 +108,7 @@ test('creates new admin token with expiry', async () => { }); }); -test('update admin token with expiry', async () => { +test('update client token with expiry', async () => { const tokenSecret = 'random-secret-update'; await db.stores.apiTokenStore.insert({ @@ -394,3 +395,17 @@ test('should create token for disabled environment', async () => { .set('Content-Type', 'application/json') .expect(201); }); + +test('updating expiry of non existing token should yield 200', async () => { + return app.request + .put('/api/admin/api-tokens/randomnonexistingsecret') + .send({ expiresAt: addDays(new Date(), 14) }) + .set('Content-Type', 'application/json') + .expect(200); +}); + +test('Deleting non-existing token should yield 200', async () => { + return app.request + .delete('/api/admin/api-tokens/random-non-existing-token') + .expect(200); +}); diff --git a/src/test/e2e/helpers/test-helper.ts b/src/test/e2e/helpers/test-helper.ts index 74ac43a4fc..7d8aa6899d 100644 --- a/src/test/e2e/helpers/test-helper.ts +++ b/src/test/e2e/helpers/test-helper.ts @@ -230,9 +230,12 @@ export async function setupAppWithAuth( export async function setupAppWithCustomAuth( stores: IUnleashStores, preHook: Function, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + customOptions?: any, ): Promise { - return createApp(stores, IAuthType.CUSTOM, preHook); + return createApp(stores, IAuthType.CUSTOM, preHook, customOptions); } + export async function setupAppWithBaseUrl( stores: IUnleashStores, ): Promise {