Update MI fields numUses, numUsesLimit, ttl, added modal for delete client secret confirmation

This commit is contained in:
Tuan Dang
2023-12-05 11:15:57 +07:00
parent 0ebd1d3d81
commit c91f6521c1
6 changed files with 128 additions and 55 deletions
@@ -44,9 +44,9 @@ const packageClientSecretData = (clientSecretData: IMachineIdentityClientSecretD
isActive: clientSecretData.isActive, isActive: clientSecretData.isActive,
description: clientSecretData.description, description: clientSecretData.description,
clientSecretPrefix: clientSecretData.clientSecretPrefix, clientSecretPrefix: clientSecretData.clientSecretPrefix,
clientSecretUsageCount: clientSecretData.clientSecretUsageCount, clientSecretNumUses: clientSecretData.clientSecretNumUses,
clientSecretUsageLimit: clientSecretData.clientSecretUsageLimit, clientSecretNumUsesLimit: clientSecretData.clientSecretNumUsesLimit,
expiresAt: clientSecretData.expiresAt clientSecretTTL: clientSecretData.clientSecretTTL
}); });
/** /**
@@ -121,7 +121,7 @@ export const createMIClientSecret = async (req: Request, res: Response) => {
body: { body: {
description, description,
ttl, ttl,
usageLimit numUsesLimit
} }
} = await validateRequest(reqValidator.CreateClientSecretV3, req); } = await validateRequest(reqValidator.CreateClientSecretV3, req);
@@ -144,11 +144,6 @@ export const createMIClientSecret = async (req: Request, res: Response) => {
if (!hasRequiredPrivileges) throw ForbiddenRequestError({ if (!hasRequiredPrivileges) throw ForbiddenRequestError({
message: "Failed to create client secret for more privileged MI" 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 clientSecret = crypto.randomBytes(32).toString("hex");
const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds()); const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds());
@@ -159,10 +154,10 @@ export const createMIClientSecret = async (req: Request, res: Response) => {
description, description,
clientSecretPrefix: clientSecret.slice(0, 4), clientSecretPrefix: clientSecret.slice(0, 4),
clientSecretHash, clientSecretHash,
clientSecretUsageCount: 0, clientSecretNumUses: 0,
clientSecretUsageLimit: usageLimit, clientSecretNumUsesLimit: numUsesLimit,
clientSecretTTL: ttl,
accessTokenVersion: 1, accessTokenVersion: 1,
expiresAt
}).save(); }).save();
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
@@ -294,27 +289,30 @@ export const loginMI = async (req: Request, res: Response) => {
if (!validatedClientSecretDatum) throw UnauthorizedRequestError(); if (!validatedClientSecretDatum) throw UnauthorizedRequestError();
const { const {
expiresAt, clientSecretTTL,
clientSecretUsageCount, clientSecretNumUses,
clientSecretUsageLimit clientSecretNumUsesLimit,
} = validatedClientSecretDatum; } = 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 // number of times client secret can be used for
// a login operation reached // a login operation reached
await MachineIdentityClientSecretData.findByIdAndUpdate( await MachineIdentityClientSecretData.findByIdAndUpdate(
@@ -334,7 +332,7 @@ export const loginMI = async (req: Request, res: Response) => {
await MachineIdentityClientSecretData.findByIdAndUpdate( await MachineIdentityClientSecretData.findByIdAndUpdate(
validatedClientSecretDatum._id, validatedClientSecretDatum._id,
{ {
$inc: { clientSecretUsageCount: 1 } $inc: { clientSecretNumUses: 1 }
}, },
{ {
new: true new: true
@@ -8,10 +8,12 @@ export interface IMachineIdentityClientSecretData extends Document {
clientSecretPrefix: string; clientSecretPrefix: string;
clientSecretHash: string; clientSecretHash: string;
clientSecretLastUsed?: Date; clientSecretLastUsed?: Date;
clientSecretUsageCount: number; clientSecretNumUses: number;
clientSecretUsageLimit: number; clientSecretNumUsesLimit: number;
clientSecretTTL: number;
accessTokenVersion: number; accessTokenVersion: number;
expiresAt?: Date; updatedAt: Date;
createdAt: Date;
} }
const machineIdentityClientSecretDataSchema = new Schema( const machineIdentityClientSecretDataSchema = new Schema(
@@ -42,29 +44,30 @@ const machineIdentityClientSecretDataSchema = new Schema(
type: Date, type: Date,
required: false required: false
}, },
clientSecretUsageCount: { clientSecretNumUses: {
// number of times client secret has been used // number of times client secret has been used
// in login operation // in login operation
type: Number, type: Number,
default: 0, default: 0,
required: true required: true
}, },
clientSecretUsageLimit: { clientSecretNumUsesLimit: {
// number of times client secret can be used for // number of times client secret can be used for
// a login operation // a login operation
type: Number, type: Number,
default: 0, // default: used as many times as needed default: 0, // default: used as many times as needed
required: true required: true
}, },
clientSecretTTL: {
type: Number,
default: 0, // default: does not expire
required: true
},
accessTokenVersion: { accessTokenVersion: {
type: Number, type: Number,
default: 1, default: 1,
required: true required: true
}, },
expiresAt: {
type: Date,
required: false
}
}, },
{ {
timestamps: true timestamps: true
+1 -1
View File
@@ -13,7 +13,7 @@ export const CreateClientSecretV3 = z.object({
}), }),
body: z.object({ body: z.object({
description: z.string().trim().default(""), 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), ttl: z.number().min(0).default(0),
}), }),
}); });
@@ -28,9 +28,11 @@ export type MachineIdentityClientSecret = {
isActive: boolean; isActive: boolean;
description: string; description: string;
clientSecretPrefix: string; clientSecretPrefix: string;
clientSecretUsageCount: number; clientSecretNumUses: number;
clientSecretUsageLimit: number; clientSecretNumUsesLimit: number;
expiresAt: string; clientSecretTTL: number;
createdAt: string;
updatedAt: string;
} }
export type MachineMembershipOrg = { export type MachineMembershipOrg = {
@@ -78,8 +80,8 @@ export type CreateMachineIdentityClientSecretRes = {
machineIdentity: string; machineIdentity: string;
isActive: boolean; isActive: boolean;
description: string; description: string;
clientSecretUsageCount: number; clientSecretNumUses: number;
clientSecretUsageLimit: number; clientSecretNumUsesLimit: number;
expiresAt?: Date; expiresAt?: Date;
} }
@@ -10,13 +10,13 @@ import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import {
Button, Button,
DeleteActionModal,
EmptyState, EmptyState,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
Modal, Modal,
ModalContent ModalContent,
,
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
@@ -41,12 +41,20 @@ const schema = yup.object({
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["clientSecret"]>; popUp: UsePopUpState<["clientSecret", "deleteClientSecret"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["clientSecret"]>, state?: boolean) => void; handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteClientSecret"]>,
data?: {
clientSecretPrefix: string;
clientSecretId: string;
}
) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["clientSecret", "deleteClientSecret"]>, state?: boolean) => void;
}; };
export const CreateClientSecretModal = ({ export const CreateClientSecretModal = ({
popUp, popUp,
handlePopUpOpen,
handlePopUpToggle handlePopUpToggle
}: Props) => { }: Props) => {
const { t } = useTranslation(); 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); const hasToken = Boolean(token);
@@ -238,20 +283,24 @@ export const CreateClientSecretModal = ({
data.map(({ data.map(({
_id, _id,
description, description,
machineIdentity, clientSecretTTL,
expiresAt,
clientSecretPrefix clientSecretPrefix
}) => { }) => {
let expiresAt;
if (clientSecretTTL > 0) {
expiresAt = new Date(new Date().getTime() + clientSecretTTL * 1000);
}
return ( return (
<Tr className="h-10" key={`mi-client-secret-${_id}`}> <Tr className="h-10" key={`mi-client-secret-${_id}`}>
<Td>{description === "" ? "-" : description}</Td> <Td>{description === "" ? "-" : description}</Td>
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td> <Td>{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}</Td>
<Td>{`${clientSecretPrefix}************`}</Td> <Td>{`${clientSecretPrefix}************`}</Td>
<Td className="flex"> <Td className="flex">
<IconButton <IconButton
onClick={async () => { onClick={() => {
await deleteClientSecretMutateAsync({ handlePopUpOpen("deleteClientSecret", {
machineId: machineIdentity, clientSecretPrefix,
clientSecretId: _id clientSecretId: _id
}); });
}} }}
@@ -277,6 +326,25 @@ export const CreateClientSecretModal = ({
</TBody> </TBody>
</Table> </Table>
</TableContainer> </TableContainer>
<DeleteActionModal
isOpen={popUp.deleteClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
(popUp?.deleteClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix || ""
}************?`}
onChange={(isOpen) => handlePopUpToggle("deleteClientSecret", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const deleteClientSecretData = (popUp?.deleteClientSecret?.data as {
clientSecretId: string;
clientSecretPrefix: string;
});
return onDeleteClientSecretSubmit({
clientSecretId: deleteClientSecretData.clientSecretId,
clientSecretPrefix: deleteClientSecretData.clientSecretPrefix
});
}}
/>
</ModalContent> </ModalContent>
</Modal> </Modal>
); );
@@ -24,6 +24,7 @@ export const MachineIdentitySection = withPermission(
"machineIdentity", "machineIdentity",
"deleteMachineIdentity", "deleteMachineIdentity",
"clientSecret", "clientSecret",
"deleteClientSecret",
"upgradePlan" "upgradePlan"
] as const); ] as const);
@@ -80,6 +81,7 @@ export const MachineIdentitySection = withPermission(
/> />
<CreateClientSecretModal <CreateClientSecretModal
popUp={popUp} popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
<DeleteActionModal <DeleteActionModal