From c91f6521c10aaa7e1c78b2e1530274552db98a9e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 5 Dec 2023 11:15:57 +0700 Subject: [PATCH] Update MI fields numUses, numUsesLimit, ttl, added modal for delete client secret confirmation --- .../v3/machineIdentityController.ts | 58 ++++++------ .../models/machineIdentityClientSecretData.ts | 21 +++-- backend/src/validation/machineIdentity.ts | 2 +- .../src/hooks/api/machineIdentities/types.ts | 12 +-- .../CreateClientSecretModal.tsx | 88 ++++++++++++++++--- .../MachineIdentitySection.tsx | 2 + 6 files changed, 128 insertions(+), 55 deletions(-) diff --git a/backend/src/ee/controllers/v3/machineIdentityController.ts b/backend/src/ee/controllers/v3/machineIdentityController.ts index 7dc439ec2..0a77c1550 100644 --- a/backend/src/ee/controllers/v3/machineIdentityController.ts +++ b/backend/src/ee/controllers/v3/machineIdentityController.ts @@ -44,9 +44,9 @@ const packageClientSecretData = (clientSecretData: IMachineIdentityClientSecretD isActive: clientSecretData.isActive, description: clientSecretData.description, clientSecretPrefix: clientSecretData.clientSecretPrefix, - clientSecretUsageCount: clientSecretData.clientSecretUsageCount, - clientSecretUsageLimit: clientSecretData.clientSecretUsageLimit, - expiresAt: clientSecretData.expiresAt + clientSecretNumUses: clientSecretData.clientSecretNumUses, + clientSecretNumUsesLimit: clientSecretData.clientSecretNumUsesLimit, + clientSecretTTL: clientSecretData.clientSecretTTL }); /** @@ -121,7 +121,7 @@ export const createMIClientSecret = async (req: Request, res: Response) => { body: { description, ttl, - usageLimit + numUsesLimit } } = await validateRequest(reqValidator.CreateClientSecretV3, req); @@ -144,11 +144,6 @@ export const createMIClientSecret = async (req: Request, res: Response) => { if (!hasRequiredPrivileges) throw ForbiddenRequestError({ message: "Failed to create client secret for more privileged MI" }); - - let expiresAt; - if (ttl > 0) { - expiresAt = new Date(new Date().getTime() + ttl * 1000); - } const clientSecret = crypto.randomBytes(32).toString("hex"); const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds()); @@ -159,10 +154,10 @@ export const createMIClientSecret = async (req: Request, res: Response) => { description, clientSecretPrefix: clientSecret.slice(0, 4), clientSecretHash, - clientSecretUsageCount: 0, - clientSecretUsageLimit: usageLimit, + clientSecretNumUses: 0, + clientSecretNumUsesLimit: numUsesLimit, + clientSecretTTL: ttl, accessTokenVersion: 1, - expiresAt }).save(); await EEAuditLogService.createAuditLog( @@ -294,27 +289,30 @@ export const loginMI = async (req: Request, res: Response) => { if (!validatedClientSecretDatum) throw UnauthorizedRequestError(); const { - expiresAt, - clientSecretUsageCount, - clientSecretUsageLimit + clientSecretTTL, + clientSecretNumUses, + clientSecretNumUsesLimit, } = validatedClientSecretDatum; - - if (expiresAt && new Date(expiresAt) < new Date()) { - // client secret expired - await MachineIdentityClientSecretData.findByIdAndUpdate( - validatedClientSecretDatum._id, - { - isActive: false - }, - { - new: true - } - ); - throw UnauthorizedRequestError(); + if (clientSecretTTL > 0) { + const expiresAt = new Date(new Date().getTime() + clientSecretTTL * 1000); + + if (expiresAt < new Date()) { + await MachineIdentityClientSecretData.findByIdAndUpdate( + validatedClientSecretDatum._id, + { + isActive: false + }, + { + new: true + } + ); + + throw UnauthorizedRequestError(); + } } - if (clientSecretUsageLimit > 0 && clientSecretUsageCount === clientSecretUsageLimit) { + if (clientSecretNumUses > 0 && clientSecretNumUses === clientSecretNumUsesLimit) { // number of times client secret can be used for // a login operation reached await MachineIdentityClientSecretData.findByIdAndUpdate( @@ -334,7 +332,7 @@ export const loginMI = async (req: Request, res: Response) => { await MachineIdentityClientSecretData.findByIdAndUpdate( validatedClientSecretDatum._id, { - $inc: { clientSecretUsageCount: 1 } + $inc: { clientSecretNumUses: 1 } }, { new: true diff --git a/backend/src/models/machineIdentityClientSecretData.ts b/backend/src/models/machineIdentityClientSecretData.ts index fbcd46606..42a6c0f44 100644 --- a/backend/src/models/machineIdentityClientSecretData.ts +++ b/backend/src/models/machineIdentityClientSecretData.ts @@ -8,10 +8,12 @@ export interface IMachineIdentityClientSecretData extends Document { clientSecretPrefix: string; clientSecretHash: string; clientSecretLastUsed?: Date; - clientSecretUsageCount: number; - clientSecretUsageLimit: number; + clientSecretNumUses: number; + clientSecretNumUsesLimit: number; + clientSecretTTL: number; accessTokenVersion: number; - expiresAt?: Date; + updatedAt: Date; + createdAt: Date; } const machineIdentityClientSecretDataSchema = new Schema( @@ -42,29 +44,30 @@ const machineIdentityClientSecretDataSchema = new Schema( type: Date, required: false }, - clientSecretUsageCount: { + clientSecretNumUses: { // number of times client secret has been used // in login operation type: Number, default: 0, required: true }, - clientSecretUsageLimit: { + clientSecretNumUsesLimit: { // number of times client secret can be used for // a login operation type: Number, default: 0, // default: used as many times as needed required: true }, + clientSecretTTL: { + type: Number, + default: 0, // default: does not expire + required: true + }, accessTokenVersion: { type: Number, default: 1, required: true }, - expiresAt: { - type: Date, - required: false - } }, { timestamps: true diff --git a/backend/src/validation/machineIdentity.ts b/backend/src/validation/machineIdentity.ts index 37bfd85c0..49347a306 100644 --- a/backend/src/validation/machineIdentity.ts +++ b/backend/src/validation/machineIdentity.ts @@ -13,7 +13,7 @@ export const CreateClientSecretV3 = z.object({ }), body: z.object({ description: z.string().trim().default(""), - usageLimit: z.number().min(0).default(0), + numUsesLimit: z.number().min(0).default(0), ttl: z.number().min(0).default(0), }), }); diff --git a/frontend/src/hooks/api/machineIdentities/types.ts b/frontend/src/hooks/api/machineIdentities/types.ts index 4e4bf326a..a54975505 100644 --- a/frontend/src/hooks/api/machineIdentities/types.ts +++ b/frontend/src/hooks/api/machineIdentities/types.ts @@ -28,9 +28,11 @@ export type MachineIdentityClientSecret = { isActive: boolean; description: string; clientSecretPrefix: string; - clientSecretUsageCount: number; - clientSecretUsageLimit: number; - expiresAt: string; + clientSecretNumUses: number; + clientSecretNumUsesLimit: number; + clientSecretTTL: number; + createdAt: string; + updatedAt: string; } export type MachineMembershipOrg = { @@ -78,8 +80,8 @@ export type CreateMachineIdentityClientSecretRes = { machineIdentity: string; isActive: boolean; description: string; - clientSecretUsageCount: number; - clientSecretUsageLimit: number; + clientSecretNumUses: number; + clientSecretNumUsesLimit: number; expiresAt?: Date; } diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx index 99b4fa70d..23a37d927 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx @@ -10,13 +10,13 @@ import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { Button, + DeleteActionModal, EmptyState, FormControl, IconButton, Input, Modal, - ModalContent -, + ModalContent, Table, TableContainer, TableSkeleton, @@ -41,12 +41,20 @@ const schema = yup.object({ export type FormData = yup.InferType; type Props = { - popUp: UsePopUpState<["clientSecret"]>; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["clientSecret"]>, state?: boolean) => void; + popUp: UsePopUpState<["clientSecret", "deleteClientSecret"]>; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deleteClientSecret"]>, + data?: { + clientSecretPrefix: string; + clientSecretId: string; + } + ) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["clientSecret", "deleteClientSecret"]>, state?: boolean) => void; }; export const CreateClientSecretModal = ({ popUp, + handlePopUpOpen, handlePopUpToggle }: Props) => { const { t } = useTranslation(); @@ -119,6 +127,43 @@ export const CreateClientSecretModal = ({ }); } } + + const onDeleteClientSecretSubmit = async ({ + clientSecretId, + clientSecretPrefix + }: { + clientSecretId: string; + clientSecretPrefix: string; + }) => { + try { + + if (!popUpData?.machineId) return; + + await deleteClientSecretMutateAsync({ + machineId: popUpData.machineId, + clientSecretId + }); + + if (token.startsWith(clientSecretPrefix)) { + reset(); + setToken(""); + } + + handlePopUpToggle("deleteClientSecret", false); + + createNotification({ + text: "Successfully deleted client secret", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete client secret", + type: "error" + }); + + } + } const hasToken = Boolean(token); @@ -238,20 +283,24 @@ export const CreateClientSecretModal = ({ data.map(({ _id, description, - machineIdentity, - expiresAt, + clientSecretTTL, clientSecretPrefix }) => { + let expiresAt; + if (clientSecretTTL > 0) { + expiresAt = new Date(new Date().getTime() + clientSecretTTL * 1000); + } + return ( {description === "" ? "-" : description} - {expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"} + {expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"} {`${clientSecretPrefix}************`} { - await deleteClientSecretMutateAsync({ - machineId: machineIdentity, + onClick={() => { + handlePopUpOpen("deleteClientSecret", { + clientSecretPrefix, clientSecretId: _id }); }} @@ -277,6 +326,25 @@ export const CreateClientSecretModal = ({ + handlePopUpToggle("deleteClientSecret", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const deleteClientSecretData = (popUp?.deleteClientSecret?.data as { + clientSecretId: string; + clientSecretPrefix: string; + }); + + return onDeleteClientSecretSubmit({ + clientSecretId: deleteClientSecretData.clientSecretId, + clientSecretPrefix: deleteClientSecretData.clientSecretPrefix + }); + }} + /> ); diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx index f006a9bfe..5e11513d7 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx @@ -24,6 +24,7 @@ export const MachineIdentitySection = withPermission( "machineIdentity", "deleteMachineIdentity", "clientSecret", + "deleteClientSecret", "upgradePlan" ] as const); @@ -80,6 +81,7 @@ export const MachineIdentitySection = withPermission( />