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:
parent
0a8330f55d
commit
054c590813
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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';
|
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
@ -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());
|
||||||
|
};
|
@ -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';
|
|
||||||
|
21
src/migrations/20230130113337-revert-user-pat-permissions.js
Normal file
21
src/migrations/20230130113337-revert-user-pat-permissions.js
Normal 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,
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user