1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-09 01:17:06 +02:00

fix: limit creation of other users PATs (adapting) (#3019)

https://linear.app/unleash/issue/2-656/limit-the-ability-of-creating-a-token-on-behalf-of-another-user

Adapts to the refactor that reverts the initial experimental idea of
Service Accounts before they existed in the current implementation:
Managing other user's PATs.
This commit is contained in:
Nuno Góis 2023-01-31 08:40:23 +00:00 committed by GitHub
parent 0a8330f55d
commit 054c590813
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 130 additions and 21 deletions

View File

@ -29,7 +29,7 @@ import {
IPersonalAPITokenFormErrors, IPersonalAPITokenFormErrors,
PersonalAPITokenForm, PersonalAPITokenForm,
} from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm'; } from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm';
import { usePersonalAPITokensApi } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi'; import { useServiceAccountTokensApi } from 'hooks/api/actions/useServiceAccountTokensApi/useServiceAccountTokensApi';
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken'; import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
import { ServiceAccountTokens } from './ServiceAccountTokens/ServiceAccountTokens'; import { ServiceAccountTokens } from './ServiceAccountTokens/ServiceAccountTokens';
import { IServiceAccount } from 'interfaces/service-account'; import { IServiceAccount } from 'interfaces/service-account';
@ -127,7 +127,7 @@ export const ServiceAccountModal = ({
const { serviceAccounts, roles, refetch } = useServiceAccounts(); const { serviceAccounts, roles, refetch } = useServiceAccounts();
const { addServiceAccount, updateServiceAccount, loading } = const { addServiceAccount, updateServiceAccount, loading } =
useServiceAccountsApi(); useServiceAccountsApi();
const { createUserPersonalAPIToken } = usePersonalAPITokensApi(); const { createServiceAccountToken } = useServiceAccountTokensApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
@ -190,7 +190,7 @@ export const ServiceAccountModal = ({
getServiceAccountPayload() getServiceAccountPayload()
); );
if (tokenGeneration === TokenGeneration.NOW) { if (tokenGeneration === TokenGeneration.NOW) {
const token = await createUserPersonalAPIToken(id, { const token = await createServiceAccountToken(id, {
description: patDescription, description: patDescription,
expiresAt: patExpiresAt, expiresAt: patExpiresAt,
}); });

View File

@ -6,7 +6,7 @@ import {
IPersonalAPITokenFormErrors, IPersonalAPITokenFormErrors,
PersonalAPITokenForm, PersonalAPITokenForm,
} from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm'; } from 'component/user/Profile/PersonalAPITokensTab/CreatePersonalAPIToken/PersonalAPITokenForm/PersonalAPITokenForm';
import { ICreatePersonalApiTokenPayload } from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi'; import { ICreateServiceAccountTokenPayload } from 'hooks/api/actions/useServiceAccountTokensApi/useServiceAccountTokensApi';
import { IPersonalAPIToken } from 'interfaces/personalAPIToken'; import { IPersonalAPIToken } from 'interfaces/personalAPIToken';
const DEFAULT_EXPIRATION = ExpirationOption['30DAYS']; const DEFAULT_EXPIRATION = ExpirationOption['30DAYS'];
@ -15,7 +15,7 @@ interface IServiceAccountCreateTokenDialogProps {
open: boolean; open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>; setOpen: React.Dispatch<React.SetStateAction<boolean>>;
tokens: IPersonalAPIToken[]; tokens: IPersonalAPIToken[];
onCreateClick: (newToken: ICreatePersonalApiTokenPayload) => void; onCreateClick: (newToken: ICreateServiceAccountTokenPayload) => void;
} }
export const ServiceAccountCreateTokenDialog = ({ export const ServiceAccountCreateTokenDialog = ({

View File

@ -16,7 +16,7 @@ import { HighlightCell } from 'component/common/Table/cells/HighlightCell/Highli
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { PAT_LIMIT } from '@server/util/constants'; import { PAT_LIMIT } from '@server/util/constants';
import { usePersonalAPITokens } from 'hooks/api/getters/usePersonalAPITokens/usePersonalAPITokens'; import { useServiceAccountTokens } from 'hooks/api/getters/useServiceAccountTokens/useServiceAccountTokens';
import { useSearch } from 'hooks/useSearch'; import { useSearch } from 'hooks/useSearch';
import { import {
INewPersonalAPIToken, INewPersonalAPIToken,
@ -32,9 +32,9 @@ import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColum
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { import {
ICreatePersonalApiTokenPayload, ICreateServiceAccountTokenPayload,
usePersonalAPITokensApi, useServiceAccountTokensApi,
} from 'hooks/api/actions/usePersonalAPITokensApi/usePersonalAPITokensApi'; } from 'hooks/api/actions/useServiceAccountTokensApi/useServiceAccountTokensApi';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { IServiceAccount } from 'interfaces/service-account'; import { IServiceAccount } from 'interfaces/service-account';
@ -90,12 +90,12 @@ export const ServiceAccountTokens = ({
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const { tokens = [], refetchTokens } = usePersonalAPITokens( const { tokens = [], refetchTokens } = useServiceAccountTokens(
serviceAccount.id serviceAccount.id
); );
const { refetch } = useServiceAccounts(); const { refetch } = useServiceAccounts();
const { createUserPersonalAPIToken, deleteUserPersonalAPIToken } = const { createServiceAccountToken, deleteServiceAccountToken } =
usePersonalAPITokensApi(); useServiceAccountTokensApi();
const [initialState] = useState(() => ({ const [initialState] = useState(() => ({
sortBy: readOnly ? [{ id: 'seenAt' }] : [defaultSort], sortBy: readOnly ? [{ id: 'seenAt' }] : [defaultSort],
@ -108,9 +108,11 @@ export const ServiceAccountTokens = ({
const [newToken, setNewToken] = useState<INewPersonalAPIToken>(); const [newToken, setNewToken] = useState<INewPersonalAPIToken>();
const [selectedToken, setSelectedToken] = useState<IPersonalAPIToken>(); const [selectedToken, setSelectedToken] = useState<IPersonalAPIToken>();
const onCreateClick = async (newToken: ICreatePersonalApiTokenPayload) => { const onCreateClick = async (
newToken: ICreateServiceAccountTokenPayload
) => {
try { try {
const token = await createUserPersonalAPIToken( const token = await createServiceAccountToken(
serviceAccount.id, serviceAccount.id,
newToken newToken
); );
@ -131,7 +133,7 @@ export const ServiceAccountTokens = ({
const onDeleteClick = async () => { const onDeleteClick = async () => {
if (selectedToken) { if (selectedToken) {
try { try {
await deleteUserPersonalAPIToken( await deleteServiceAccountToken(
serviceAccount.id, serviceAccount.id,
selectedToken?.id selectedToken?.id
); );

View File

@ -36,6 +36,3 @@ export const DELETE_SEGMENT = 'DELETE_SEGMENT';
export const APPLY_CHANGE_REQUEST = 'APPLY_CHANGE_REQUEST'; export const APPLY_CHANGE_REQUEST = 'APPLY_CHANGE_REQUEST';
export const APPROVE_CHANGE_REQUEST = 'APPROVE_CHANGE_REQUEST'; export const APPROVE_CHANGE_REQUEST = 'APPROVE_CHANGE_REQUEST';
export const SKIP_CHANGE_REQUEST = 'SKIP_CHANGE_REQUEST'; export const SKIP_CHANGE_REQUEST = 'SKIP_CHANGE_REQUEST';
export const READ_USER_PAT = 'READ_USER_PAT';
export const CREATE_USER_PAT = 'CREATE_USER_PAT';
export const DELETE_USER_PAT = 'DELETE_USER_PAT';

View File

@ -0,0 +1,56 @@
import { INewPersonalAPIToken } from 'interfaces/personalAPIToken';
import useAPI from '../useApi/useApi';
export interface ICreateServiceAccountTokenPayload {
description: string;
expiresAt: Date;
}
export const useServiceAccountTokensApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
});
const createServiceAccountToken = async (
serviceAccountId: number,
payload: ICreateServiceAccountTokenPayload
): Promise<INewPersonalAPIToken> => {
const req = createRequest(
`api/admin/service-account/${serviceAccountId}/token`,
{
method: 'POST',
body: JSON.stringify(payload),
}
);
try {
const response = await makeRequest(req.caller, req.id);
return await response.json();
} catch (e) {
throw e;
}
};
const deleteServiceAccountToken = async (
serviceAccountId: number,
id: string
) => {
const req = createRequest(
`api/admin/service-account/${serviceAccountId}/token/${id}`,
{
method: 'DELETE',
}
);
try {
await makeRequest(req.caller, req.id);
} catch (e) {
throw e;
}
};
return {
createServiceAccountToken,
deleteServiceAccountToken,
errors,
loading,
};
};

View File

@ -0,0 +1,36 @@
import { IPersonalAPIToken } from 'interfaces/personalAPIToken';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
import useUiConfig from '../useUiConfig/useUiConfig';
export interface IUseServiceAccountTokensOutput {
tokens?: IPersonalAPIToken[];
refetchTokens: () => void;
loading: boolean;
error?: Error;
}
export const useServiceAccountTokens = (id: number) => {
const { uiConfig, isEnterprise } = useUiConfig();
const { data, error, mutate } = useConditionalSWR(
Boolean(uiConfig.flags.serviceAccounts) && isEnterprise(),
{ tokens: [] },
formatApiPath(`api/admin/service-account/${id}/token`),
fetcher
);
return {
tokens: data ? data.pats : undefined,
loading: !error && !data,
refetchTokens: () => mutate(),
error,
};
};
const fetcher = (path: string) => {
return fetch(path)
.then(handleErrorResponses('Service Account Tokens'))
.then(res => res.json());
};

View File

@ -42,6 +42,3 @@ export const DELETE_SEGMENT = 'DELETE_SEGMENT';
export const APPROVE_CHANGE_REQUEST = 'APPROVE_CHANGE_REQUEST'; export const APPROVE_CHANGE_REQUEST = 'APPROVE_CHANGE_REQUEST';
export const APPLY_CHANGE_REQUEST = 'APPLY_CHANGE_REQUEST'; export const APPLY_CHANGE_REQUEST = 'APPLY_CHANGE_REQUEST';
export const SKIP_CHANGE_REQUEST = 'SKIP_CHANGE_REQUEST'; export const SKIP_CHANGE_REQUEST = 'SKIP_CHANGE_REQUEST';
export const READ_USER_PAT = 'READ_USER_PAT';
export const CREATE_USER_PAT = 'CREATE_USER_PAT';
export const DELETE_USER_PAT = 'DELETE_USER_PAT';

View File

@ -0,0 +1,21 @@
exports.up = function (db, cb) {
db.runSql(
`
DELETE FROM permissions WHERE permission = 'READ_USER_PAT';
DELETE FROM permissions WHERE permission = 'CREATE_USER_PAT';
DELETE FROM permissions WHERE permission = 'DELETE_USER_PAT';
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
INSERT INTO permissions (permission, display_name, type) VALUES ('READ_USER_PAT', 'Read PATs for a specific user', 'root');
INSERT INTO permissions (permission, display_name, type) VALUES ('CREATE_USER_PAT', 'Create a PAT for a specific user', 'root');
INSERT INTO permissions (permission, display_name, type) VALUES ('DELETE_USER_PAT', 'Delete a PAT for a specific user', 'root');
`,
cb,
);
};