mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 21:51:56 +00:00
Clean up and weave crypto into service account permissions
This commit is contained in:
@@ -3,11 +3,13 @@ import { useRouter } from 'next/router';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
|
||||
import { SAProjectLevelPermissionsTable } from './components/SAProjectLevelPermissionsTable';
|
||||
import { ServiceAccountNameChangeSection } from './components';
|
||||
import {
|
||||
CopyServiceAccountIDSection,
|
||||
ServiceAccountNameChangeSection} from './components';
|
||||
|
||||
export const CreateServiceAccountPage = () => {
|
||||
const router = useRouter();
|
||||
const { serviceAccountId }: { serviceAccountId: string } = router.query;
|
||||
const {serviceAccountId} = router.query;
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
@@ -21,23 +23,23 @@ export const CreateServiceAccountPage = () => {
|
||||
A service account represents a machine identity such as a VM or application client.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-8xl mx-6">
|
||||
{typeof serviceAccountId === 'string' && (
|
||||
{typeof serviceAccountId === 'string' && (
|
||||
<div className="max-w-8xl mx-6">
|
||||
<ServiceAccountNameChangeSection
|
||||
serviceAccountId={serviceAccountId}
|
||||
/>
|
||||
)}
|
||||
{/* <div className="rounded-md bg-white/5 p-6 mt-6">
|
||||
<p className="mb-4 text-xl font-semibold">Organization-Level Permissions</p>
|
||||
<SAProjectLevelPermissionsTable />
|
||||
</div> */}
|
||||
<div className="rounded-md bg-white/5 p-6 mt-6">
|
||||
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
|
||||
<SAProjectLevelPermissionsTable
|
||||
serviceAccountId={serviceAccountId}
|
||||
/>
|
||||
<div className="mt-8">
|
||||
<CopyServiceAccountIDSection
|
||||
serviceAccountId={serviceAccountId}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<SAProjectLevelPermissionsTable
|
||||
serviceAccountId={serviceAccountId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { useEffect } from 'react';
|
||||
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { IconButton } from '@app/components/v2';
|
||||
import { useToggle } from '@app/hooks';
|
||||
|
||||
type Props = {
|
||||
serviceAccountId: string;
|
||||
}
|
||||
|
||||
export const CopyServiceAccountIDSection = ({ serviceAccountId }: Props): JSX.Element => {
|
||||
const [isServiceAccountIdCopied, setIsServiceAccountIdCopied] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
if (isServiceAccountIdCopied) {
|
||||
timer = setTimeout(() => setIsServiceAccountIdCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isServiceAccountIdCopied]);
|
||||
|
||||
const copyServiceAccountIdToClipboard = () => {
|
||||
navigator.clipboard.writeText(serviceAccountId);
|
||||
setIsServiceAccountIdCopied.on();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-start rounded-md bg-white/5 px-6 p-6">
|
||||
<p className="text-xl font-semibold">Service Account ID</p>
|
||||
<div className="mt-4 flex items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400 p-2">
|
||||
<p className="mr-4 break-all">{serviceAccountId}</p>
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
onClick={() => copyServiceAccountIdToClipboard()}
|
||||
>
|
||||
<FontAwesomeIcon icon={isServiceAccountIdCopied ? faCheck : faCopy} />
|
||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
Copy
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { CopyServiceAccountIDSection } from './CopyServiceAccountIDSection';
|
||||
+85
-29
@@ -9,6 +9,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric,
|
||||
verifyPrivateKey} from '@app/components/utilities/cryptography/crypto';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -32,12 +36,15 @@ import {
|
||||
Tr} from '@app/components/v2';
|
||||
import { usePopUp } from '@app/hooks';
|
||||
import {
|
||||
useCreateServiceAccountProjectLevelPermissions,
|
||||
useDeleteServiceAccountProjectLevelPermissions,
|
||||
useCreateServiceAccountProjectLevelPermission,
|
||||
useDeleteServiceAccountProjectLevelPermission,
|
||||
useGetServiceAccountById,
|
||||
useGetServiceAccountProjectLevelPermissions,
|
||||
useGetUserWorkspaces} from '@app/hooks/api';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
const createProjectLevelPermissionSchema = yup.object({
|
||||
privateKey: yup.string().required().label('Private Key'),
|
||||
workspace: yup.string().required().label('Workspace'),
|
||||
environment: yup.string().required().label('Environment'),
|
||||
permissions: yup.object().shape({
|
||||
@@ -57,18 +64,19 @@ type Props = {
|
||||
export const SAProjectLevelPermissionsTable = ({
|
||||
serviceAccountId
|
||||
}: Props) => {
|
||||
const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
|
||||
const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
|
||||
const [searchPermissions, setSearchPermissions] = useState('');
|
||||
const [defaultValues, setDefaultValues] = useState<CreateProjectLevelPermissionForm | undefined>(undefined);
|
||||
|
||||
const { data: permissions, isLoading: isPermissionsLoading } = useGetServiceAccountProjectLevelPermissions(serviceAccountId);
|
||||
const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } = useGetServiceAccountProjectLevelPermissions(serviceAccountId);
|
||||
|
||||
const createServiceAccountProjectLevelPermissions = useCreateServiceAccountProjectLevelPermissions();
|
||||
const deleteServiceAccountProjectLevelPermissions = useDeleteServiceAccountProjectLevelPermissions();
|
||||
const createServiceAccountProjectLevelPermission = useCreateServiceAccountProjectLevelPermission();
|
||||
const deleteServiceAccountProjectLevelPermission = useDeleteServiceAccountProjectLevelPermission();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
'addProjectLevelPermissions',
|
||||
'removeProjectLevelPermissions',
|
||||
'addProjectLevelPermission',
|
||||
'removeProjectLevelPermission',
|
||||
] as const);
|
||||
|
||||
const {
|
||||
@@ -78,35 +86,68 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
formState: { isSubmitting }
|
||||
} = useForm<CreateProjectLevelPermissionForm>({ resolver: yupResolver(createProjectLevelPermissionSchema), defaultValues })
|
||||
|
||||
const onAddProjectLevelPermissions = async ({
|
||||
const onAddProjectLevelPermission = async ({
|
||||
privateKey,
|
||||
workspace,
|
||||
environment,
|
||||
permissions: { canRead, canWrite, canUpdate, canDelete }
|
||||
}: CreateProjectLevelPermissionForm) => {
|
||||
await createServiceAccountProjectLevelPermissions.mutateAsync({
|
||||
|
||||
// TODO: clean up / modularize this function
|
||||
|
||||
if (!serviceAccount) return;
|
||||
|
||||
const { latestKey } = await getLatestFileKey({
|
||||
workspaceId: workspace
|
||||
});
|
||||
|
||||
verifyPrivateKey({
|
||||
privateKey,
|
||||
publicKey: serviceAccount.publicKey
|
||||
});
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: serviceAccount.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await createServiceAccountProjectLevelPermission.mutateAsync({
|
||||
serviceAccountId,
|
||||
workspaceId: workspace,
|
||||
environment,
|
||||
canRead,
|
||||
canWrite,
|
||||
canUpdate,
|
||||
canDelete
|
||||
canDelete,
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
});
|
||||
handlePopUpClose('addProjectLevelPermissions');
|
||||
handlePopUpClose('addProjectLevelPermission');
|
||||
}
|
||||
|
||||
const onRemoveProjectLevelPermissions = async () => {
|
||||
const serviceAccountWorkspacePermissionsId = (popUp?.removeProjectLevelPermissions?.data as { _id: string })?._id;
|
||||
await deleteServiceAccountProjectLevelPermissions.mutateAsync({
|
||||
const onRemoveProjectLevelPermission = async () => {
|
||||
const serviceAccountWorkspacePermissionId = (popUp?.removeProjectLevelPermission?.data as { _id: string })?._id;
|
||||
await deleteServiceAccountProjectLevelPermission.mutateAsync({
|
||||
serviceAccountId,
|
||||
serviceAccountWorkspacePermissionsId
|
||||
serviceAccountWorkspacePermissionId
|
||||
});
|
||||
handlePopUpClose('removeProjectLevelPermissions');
|
||||
handlePopUpClose('removeProjectLevelPermission');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (userWorkspaces) {
|
||||
setDefaultValues({
|
||||
privateKey: '',
|
||||
workspace: String(userWorkspaces?.[0]?._id),
|
||||
environment: String(userWorkspaces?.[0]?.environments?.[0]?.slug),
|
||||
permissions: {
|
||||
@@ -118,10 +159,10 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
});
|
||||
}
|
||||
}, [userWorkspaces]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="w-full bg-white/5 p-6">
|
||||
<p className="mb-4 text-xl font-semibold">Project-Level Permissions</p>
|
||||
<div className="mb-4 flex">
|
||||
<div className="mr-4 flex-1">
|
||||
<Input
|
||||
@@ -134,7 +175,7 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen('addProjectLevelPermissions')
|
||||
handlePopUpOpen('addProjectLevelPermission')
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
@@ -156,8 +197,8 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPermissionsLoading && <TableSkeleton columns={6} key="service-account-project-level-permissions" />}
|
||||
{!isPermissionsLoading && permissions && (
|
||||
permissions.map(({
|
||||
{!isPermissionsLoading && serviceAccountWorkspacePermissions && (
|
||||
serviceAccountWorkspacePermissions.map(({
|
||||
_id,
|
||||
workspace,
|
||||
environment,
|
||||
@@ -196,13 +237,14 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
<Checkbox
|
||||
id="isDeletePermissionEnabled"
|
||||
isChecked={canDelete}
|
||||
isDisabled
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen('removeProjectLevelPermissions', { _id })}
|
||||
onClick={() => handlePopUpOpen('removeProjectLevelPermission', { _id })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
@@ -211,7 +253,7 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
);
|
||||
})
|
||||
)}
|
||||
{!isPermissionsLoading && permissions?.length === 0 && (
|
||||
{!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={7} className="py-6 text-center text-bunker-400">
|
||||
<EmptyState title="No permissions found" icon={faKey} />
|
||||
@@ -222,18 +264,32 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Modal
|
||||
isOpen={popUp?.addProjectLevelPermissions?.isOpen}
|
||||
isOpen={popUp?.addProjectLevelPermission?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle('addProjectLevelPermissions', isOpen);
|
||||
handlePopUpToggle('addProjectLevelPermission', isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add a Project-Level Permission"
|
||||
subTitle="The service account will be granted scoped access to the specified project and environment"
|
||||
>
|
||||
<form onSubmit={handleSubmit(onAddProjectLevelPermissions)}>
|
||||
<form onSubmit={handleSubmit(onAddProjectLevelPermission)}>
|
||||
{!isUserWorkspacesLoading && userWorkspaces && (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="privateKey"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Service Account Private Key"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="workspace"
|
||||
@@ -383,11 +439,11 @@ export const SAProjectLevelPermissionsTable = ({
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeProjectLevelPermissions.isOpen}
|
||||
isOpen={popUp.removeProjectLevelPermission.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this permission from the service account?"
|
||||
onChange={(isOpen) => handlePopUpToggle('removeProjectLevelPermissions', isOpen)}
|
||||
onDeleteApproved={onRemoveProjectLevelPermissions}
|
||||
onChange={(isOpen) => handlePopUpToggle('removeProjectLevelPermission', isOpen)}
|
||||
onDeleteApproved={onRemoveProjectLevelPermission}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { CopyServiceAccountIDSection } from './CopyServiceAccountIDSection';
|
||||
export { SAProjectLevelPermissionsTable } from './SAProjectLevelPermissionsTable';
|
||||
export { ServiceAccountNameChangeSection } from './ServiceAccountNameChangeSection';
|
||||
-10
@@ -62,8 +62,6 @@ export const OrgServiceAccountsTable = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
console.log('currentWorkspace: ', currentWorkspace);
|
||||
|
||||
const orgId = currentOrg?._id || '';
|
||||
const [step, setStep] = useState(0);
|
||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||
@@ -114,8 +112,6 @@ export const OrgServiceAccountsTable = () => {
|
||||
expiresIn
|
||||
});
|
||||
|
||||
console.log('serviceAccountDetails: ', serviceAccountDetails);
|
||||
|
||||
setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||
|
||||
setStep(1);
|
||||
@@ -123,11 +119,7 @@ export const OrgServiceAccountsTable = () => {
|
||||
}
|
||||
|
||||
const onRemoveServiceAccount = async () => {
|
||||
console.log('onRemoveServiceAccount');
|
||||
|
||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||
console.log('serviceAccountId: ', serviceAccountId);
|
||||
|
||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||
handlePopUpClose('removeServiceAccount');
|
||||
}
|
||||
@@ -247,8 +239,6 @@ export const OrgServiceAccountsTable = () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('serviceAccounts: ', serviceAccounts);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex">
|
||||
|
||||
Reference in New Issue
Block a user