diff --git a/frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts b/frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts index b2c16b941c0..71422f61a30 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts +++ b/frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts @@ -7,6 +7,7 @@ import { ADMIN, CREATE_FRONTEND_API_TOKEN, CREATE_CLIENT_API_TOKEN, + CREATE_PROJECT_API_TOKEN, } from '@server/types/permissions'; import { useHasRootAccess } from 'hooks/useHasAccess'; import { SelectOption } from './TokenTypeSelector/TokenTypeSelector'; @@ -17,17 +18,28 @@ export const useApiTokenForm = (project?: string) => { const { uiConfig } = useUiConfig(); const initialEnvironment = environments?.find(e => e.enabled)?.name; + const hasCreateTokenPermission = useHasRootAccess(CREATE_CLIENT_API_TOKEN); + const hasCreateProjectTokenPermission = useHasRootAccess( + CREATE_PROJECT_API_TOKEN, + project + ); + const apiTokenTypes: SelectOption[] = [ { key: TokenType.CLIENT, label: `Server-side SDK (${TokenType.CLIENT})`, title: 'Connect server-side SDK or Unleash Proxy', - enabled: useHasRootAccess(CREATE_CLIENT_API_TOKEN), + enabled: + hasCreateTokenPermission || hasCreateProjectTokenPermission, }, ]; const hasAdminAccess = useHasRootAccess(ADMIN); const hasCreateFrontendAccess = useHasRootAccess(CREATE_FRONTEND_API_TOKEN); + const hasCreateFrontendTokenAccess = useHasRootAccess( + CREATE_PROJECT_API_TOKEN, + project + ); if (!project) { apiTokenTypes.push({ key: TokenType.ADMIN, @@ -42,7 +54,7 @@ export const useApiTokenForm = (project?: string) => { key: TokenType.FRONTEND, label: `Client-side SDK (${TokenType.FRONTEND})`, title: 'Connect web and mobile SDK directly to Unleash', - enabled: hasCreateFrontendAccess, + enabled: hasCreateFrontendAccess || hasCreateFrontendTokenAccess, }); } diff --git a/src/lib/routes/admin-api/project/api-token.ts b/src/lib/routes/admin-api/project/api-token.ts index fbd14e0f0e4..3b6b0013e17 100644 --- a/src/lib/routes/admin-api/project/api-token.ts +++ b/src/lib/routes/admin-api/project/api-token.ts @@ -34,7 +34,6 @@ import { Response } from 'express'; import { timingSafeEqual } from 'crypto'; import { createApiToken } from '../../../schema/api-token-schema'; import { OperationDeniedError } from '../../../error'; -import { tokenTypeToCreatePermission } from '../api-token'; interface ProjectTokenParam { token: string; @@ -159,9 +158,7 @@ export class ProjectApiTokenController extends Controller { ): Promise { const createToken = await createApiToken.validateAsync(req.body); const { projectId } = req.params; - const permissionRequired = tokenTypeToCreatePermission( - createToken.type, - ); + const permissionRequired = CREATE_PROJECT_API_TOKEN; const hasPermission = await this.accessService.hasPermission( req.user, permissionRequired, 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 5c154843d93..20c2d24cf54 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 @@ -5,6 +5,7 @@ import { ApiTokenType } from '../../../../lib/types/models/api-token'; import { RoleName } from '../../../../lib/types/model'; import { CREATE_CLIENT_API_TOKEN, + CREATE_PROJECT_API_TOKEN, DELETE_CLIENT_API_TOKEN, READ_CLIENT_API_TOKEN, READ_FRONTEND_API_TOKEN, @@ -171,6 +172,60 @@ test('Token-admin should be allowed to create token', async () => { await destroy(); }); +test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', async () => { + expect.assertions(0); + + 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: 'powerpuffgirls_viewer@example.com', + rootRole: role.id, + }); + req.user = user; + const createClientApiTokenRole = await accessService.createRole({ + name: 'project_client_token_creator', + description: 'Can create client tokens', + permissions: [], + type: 'root-custom', + }); + await accessService.addPermissionToRole( + role.id, + CREATE_PROJECT_API_TOKEN, + ); + await accessService.addUserToRole( + user.id, + createClientApiTokenRole.id, + 'default', + ); + req.user = await userService.createUser({ + email: 'someguyinplaces@example.com', + rootRole: role.id, + }); + next(); + }); + }; + + const { request, destroy } = await setupAppWithCustomAuth(stores, preHook, { + experimental: { + flags: { + customRootRoles: true, + }, + }, + }); + + await request + .post('/api/admin/projects/default/api-tokens') + .send({ + username: 'client-token-maker', + type: 'client', + projects: ['default'], + }) + .set('Content-Type', 'application/json') + .expect(201); + 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 () => {