mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
Feat/more granular permissions check in create apitoken (#4072)
## About the changes This PR enables or disables create API token button based on the permissions. **Note:** the button is only displayed if you have READ permissions on some API token. This is a minor limitation as having CREATE permissions should also grant READ permissions, but right now this is up to the user to set up the custom role with the correct permissions **Note 2:** Project-specific API tokens are also ruled by the project-specific permission to create API tokens in a project (just having the root permissions to create a client token or frontend token does not grant access to create a project-specific API token). The permissions to access the creation of a project-specific API token then rely on the root permissions to allow the user to create either a client token or a frontend token. --------- Co-authored-by: David Leek <david@getunleash.io>
This commit is contained in:
parent
c81de4a5bc
commit
89cf16f915
@ -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 (
|
||||
<StyledContainer>
|
||||
<FormControl sx={{ mb: 2, width: '100%' }}>
|
||||
@ -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 }) => (
|
||||
<FormControlLabel
|
||||
key={key}
|
||||
value={key}
|
||||
sx={{ mb: 1 }}
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
ml: 0.75,
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
{apiTokenTypes.map(
|
||||
({ key, label, title, enabled: hasAccess }) => (
|
||||
<FormControlLabel
|
||||
key={key}
|
||||
value={key}
|
||||
sx={{ mb: 1 }}
|
||||
disabled={!hasAccess}
|
||||
control={
|
||||
<Radio
|
||||
sx={{
|
||||
ml: 0.75,
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{label}</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Box>
|
||||
<Typography>{label}</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</StyledContainer>
|
||||
|
@ -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<string[]>([
|
||||
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,
|
||||
|
@ -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 = () => {
|
||||
/>
|
||||
<PageHeader.Divider />
|
||||
<CreateApiTokenButton
|
||||
permission={ADMIN}
|
||||
permission={[
|
||||
CREATE_FRONTEND_API_TOKEN,
|
||||
CREATE_CLIENT_API_TOKEN,
|
||||
ADMIN,
|
||||
]}
|
||||
path="/admin/api/create-token"
|
||||
/>
|
||||
</>
|
||||
|
@ -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={<CreateButton name="token" permission={ADMIN} />}
|
||||
actions={
|
||||
<CreateButton
|
||||
name="token"
|
||||
permission={[
|
||||
ADMIN,
|
||||
CREATE_CLIENT_API_TOKEN,
|
||||
CREATE_FRONTEND_API_TOKEN,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TokenInfo
|
||||
username={username}
|
||||
@ -113,7 +127,11 @@ export const CreateApiToken = ({ modal = false }: ICreateApiTokenProps) => {
|
||||
errors={errors}
|
||||
clearErrors={clearErrors}
|
||||
/>
|
||||
<TokenTypeSelector type={type} setType={setTokenType} />
|
||||
<TokenTypeSelector
|
||||
type={type}
|
||||
setType={setTokenType}
|
||||
apiTokenTypes={apiTokenTypes}
|
||||
/>
|
||||
<ProjectSelector
|
||||
type={type}
|
||||
projects={projects}
|
||||
|
@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Add } from '@mui/icons-material';
|
||||
interface ICreateApiTokenButton {
|
||||
path: string;
|
||||
permission: string;
|
||||
permission: string | string[];
|
||||
project?: string;
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ interface IResponsiveButtonProps {
|
||||
tooltipProps?: Omit<ITooltipResolverProps, 'children'>;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
permission: string;
|
||||
permission: string | string[];
|
||||
projectId?: string;
|
||||
environmentId?: string;
|
||||
maxWidth: string;
|
||||
|
@ -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}
|
||||
/>
|
||||
<TokenTypeSelector type={type} setType={setTokenType} />
|
||||
<TokenTypeSelector
|
||||
type={type}
|
||||
setType={setTokenType}
|
||||
apiTokenTypes={apiTokenTypes}
|
||||
/>
|
||||
<EnvironmentSelector
|
||||
type={type}
|
||||
environment={environment}
|
||||
|
@ -45,9 +45,9 @@ import { OperationDeniedError } from '../../error';
|
||||
interface TokenParam {
|
||||
token: string;
|
||||
}
|
||||
const tokenTypeToCreatePermission: (tokenType: ApiTokenType) => string = (
|
||||
tokenType,
|
||||
) => {
|
||||
export const tokenTypeToCreatePermission: (
|
||||
tokenType: ApiTokenType,
|
||||
) => string = (tokenType) => {
|
||||
switch (tokenType) {
|
||||
case ApiTokenType.ADMIN:
|
||||
return ADMIN;
|
||||
|
@ -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<any> {
|
||||
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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user