diff --git a/frontend/src/component/admin/apiToken/ApiTokenForm/TokenTypeSelector/TokenTypeSelector.tsx b/frontend/src/component/admin/apiToken/ApiTokenForm/TokenTypeSelector/TokenTypeSelector.tsx
index ce4ca76e68..27553d19aa 100644
--- a/frontend/src/component/admin/apiToken/ApiTokenForm/TokenTypeSelector/TokenTypeSelector.tsx
+++ b/frontend/src/component/admin/apiToken/ApiTokenForm/TokenTypeSelector/TokenTypeSelector.tsx
@@ -7,45 +7,25 @@ import {
RadioGroup,
Typography,
} from '@mui/material';
-import React from 'react';
-import { TokenType } from '../../../../../interfaces/token';
-import useUiConfig from '../../../../../hooks/api/getters/useUiConfig/useUiConfig';
-import { useOptionalPathParam } from '../../../../../hooks/useOptionalPathParam';
+import { TokenType } from 'interfaces/token';
+
+export type SelectOption = {
+ key: string;
+ label: string;
+ title: string;
+ enabled: boolean;
+};
interface ITokenTypeSelectorProps {
type: string;
- setType: (value: string) => void;
+ setType: (value: TokenType) => void;
+ apiTokenTypes: SelectOption[];
}
export const TokenTypeSelector = ({
type,
setType,
+ apiTokenTypes,
}: ITokenTypeSelectorProps) => {
- const projectId = useOptionalPathParam('projectId');
- const { uiConfig } = useUiConfig();
-
- const selectableTypes = [
- {
- key: TokenType.CLIENT,
- label: `Server-side SDK (${TokenType.CLIENT})`,
- title: 'Connect server-side SDK or Unleash Proxy',
- },
- ];
-
- if (!projectId) {
- selectableTypes.push({
- key: TokenType.ADMIN,
- label: TokenType.ADMIN,
- title: 'Full access for managing Unleash',
- });
- }
-
- if (uiConfig.flags.embedProxyFrontend) {
- selectableTypes.splice(1, 0, {
- key: TokenType.FRONTEND,
- label: `Client-side SDK (${TokenType.FRONTEND})`,
- title: 'Connect web and mobile SDK directly to Unleash',
- });
- }
return (
@@ -57,36 +37,39 @@ export const TokenTypeSelector = ({
defaultValue="CLIENT"
name="radio-buttons-group"
value={type}
- onChange={(event, value) => setType(value)}
+ onChange={(_, value) => setType(value as TokenType)}
>
- {selectableTypes.map(({ key, label, title }) => (
-
- }
- label={
-
+ {apiTokenTypes.map(
+ ({ key, label, title, enabled: hasAccess }) => (
+
+ }
+ label={
- {label}
-
- {title}
-
+
+ {label}
+
+ {title}
+
+
-
- }
- />
- ))}
+ }
+ />
+ )
+ )}
diff --git a/frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts b/frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts
index 04e5cd84d9..b2c16b941c 100644
--- a/frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts
+++ b/frontend/src/component/admin/apiToken/ApiTokenForm/useApiTokenForm.ts
@@ -1,14 +1,55 @@
import { useEffect, useState } from 'react';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import { IApiTokenCreate } from 'hooks/api/actions/useApiTokensApi/useApiTokensApi';
+import { TokenType } from 'interfaces/token';
+import {
+ ADMIN,
+ CREATE_FRONTEND_API_TOKEN,
+ CREATE_CLIENT_API_TOKEN,
+} from '@server/types/permissions';
+import { useHasRootAccess } from 'hooks/useHasAccess';
+import { SelectOption } from './TokenTypeSelector/TokenTypeSelector';
export type ApiTokenFormErrorType = 'username' | 'projects';
export const useApiTokenForm = (project?: string) => {
const { environments } = useEnvironments();
+ const { uiConfig } = useUiConfig();
const initialEnvironment = environments?.find(e => e.enabled)?.name;
+ 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),
+ },
+ ];
+
+ const hasAdminAccess = useHasRootAccess(ADMIN);
+ const hasCreateFrontendAccess = useHasRootAccess(CREATE_FRONTEND_API_TOKEN);
+ if (!project) {
+ apiTokenTypes.push({
+ key: TokenType.ADMIN,
+ label: TokenType.ADMIN,
+ title: 'Full access for managing Unleash',
+ enabled: hasAdminAccess,
+ });
+ }
+
+ if (uiConfig.flags.embedProxyFrontend) {
+ apiTokenTypes.splice(1, 0, {
+ key: TokenType.FRONTEND,
+ label: `Client-side SDK (${TokenType.FRONTEND})`,
+ title: 'Connect web and mobile SDK directly to Unleash',
+ enabled: hasCreateFrontendAccess,
+ });
+ }
+
+ const firstAccessibleType = apiTokenTypes.find(t => t.enabled)?.key;
+
const [username, setUsername] = useState('');
- const [type, setType] = useState('CLIENT');
+ const [type, setType] = useState(firstAccessibleType || TokenType.CLIENT);
const [projects, setProjects] = useState([
project ? project : '*',
]);
@@ -23,9 +64,9 @@ export const useApiTokenForm = (project?: string) => {
setEnvironment(type === 'ADMIN' ? '*' : initialEnvironment);
}, [type, initialEnvironment]);
- const setTokenType = (value: string) => {
+ const setTokenType = (value: TokenType) => {
if (value === 'ADMIN') {
- setType(value);
+ setType(TokenType.ADMIN);
setMemorizedProjects(projects);
setProjects(['*']);
setEnvironment('*');
@@ -69,6 +110,7 @@ export const useApiTokenForm = (project?: string) => {
return {
username,
type,
+ apiTokenTypes,
projects,
environment,
setUsername,
diff --git a/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx b/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx
index 84ccb0c4f5..fa0007fb9f 100644
--- a/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx
+++ b/frontend/src/component/admin/apiToken/ApiTokenPage/ApiTokenPage.tsx
@@ -19,6 +19,8 @@ import {
DELETE_FRONTEND_API_TOKEN,
READ_CLIENT_API_TOKEN,
READ_FRONTEND_API_TOKEN,
+ CREATE_CLIENT_API_TOKEN,
+ CREATE_FRONTEND_API_TOKEN,
} from '@server/types/permissions';
export const ApiTokenPage = () => {
@@ -88,7 +90,11 @@ export const ApiTokenPage = () => {
/>
>
diff --git a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx
index e2be813b02..51b0baf7bd 100644
--- a/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx
+++ b/frontend/src/component/admin/apiToken/CreateApiToken/CreateApiToken.tsx
@@ -17,7 +17,11 @@ import { TokenInfo } from '../ApiTokenForm/TokenInfo/TokenInfo';
import { TokenTypeSelector } from '../ApiTokenForm/TokenTypeSelector/TokenTypeSelector';
import { ProjectSelector } from '../ApiTokenForm/ProjectSelector/ProjectSelector';
import { EnvironmentSelector } from '../ApiTokenForm/EnvironmentSelector/EnvironmentSelector';
-import { ADMIN } from '@server/types/permissions';
+import {
+ ADMIN,
+ CREATE_CLIENT_API_TOKEN,
+ CREATE_FRONTEND_API_TOKEN,
+} from '@server/types/permissions';
const pageTitle = 'Create API token';
interface ICreateApiTokenProps {
@@ -43,6 +47,7 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
isValid,
errors,
clearErrors,
+ apiTokenTypes,
} = useApiTokenForm();
const { createToken, loading } = useApiTokensApi();
@@ -105,7 +110,16 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
handleSubmit={handleSubmit}
handleCancel={handleCancel}
mode="Create"
- actions={}
+ actions={
+
+ }
>
{
errors={errors}
clearErrors={clearErrors}
/>
-
+
;
onClick: () => void;
disabled?: boolean;
- permission: string;
+ permission: string | string[];
projectId?: string;
environmentId?: string;
maxWidth: string;
diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiTokenForm.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiTokenForm.tsx
index 92594f236a..1bea96da97 100644
--- a/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiTokenForm.tsx
+++ b/frontend/src/component/project/Project/ProjectSettings/ProjectApiAccess/CreateProjectApiTokenForm.tsx
@@ -36,6 +36,7 @@ export const CreateProjectApiTokenForm = () => {
getApiTokenPayload,
username,
type,
+ apiTokenTypes,
environment,
setUsername,
setTokenType,
@@ -126,7 +127,11 @@ export const CreateProjectApiTokenForm = () => {
errors={errors}
clearErrors={clearErrors}
/>
-
+
string = (
- tokenType,
-) => {
+export const tokenTypeToCreatePermission: (
+ tokenType: ApiTokenType,
+) => string = (tokenType) => {
switch (tokenType) {
case ApiTokenType.ADMIN:
return ADMIN;
diff --git a/src/lib/routes/admin-api/project/api-token.ts b/src/lib/routes/admin-api/project/api-token.ts
index 1b2277ce5c..fbd14e0f0e 100644
--- a/src/lib/routes/admin-api/project/api-token.ts
+++ b/src/lib/routes/admin-api/project/api-token.ts
@@ -33,6 +33,8 @@ import { Logger } from '../../../logger';
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;
@@ -157,6 +159,19 @@ export class ProjectApiTokenController extends Controller {
): Promise {
const createToken = await createApiToken.validateAsync(req.body);
const { projectId } = req.params;
+ const permissionRequired = tokenTypeToCreatePermission(
+ createToken.type,
+ );
+ const hasPermission = await this.accessService.hasPermission(
+ req.user,
+ permissionRequired,
+ projectId,
+ );
+ if (!hasPermission) {
+ throw new OperationDeniedError(
+ `You don't have the necessary access [${permissionRequired}] to perform this operation]`,
+ );
+ }
if (!createToken.project) {
createToken.project = projectId;
}