requested changes & edge cases

This commit is contained in:
Daniel Hougaard
2025-04-26 05:19:02 +04:00
parent 9ea6eca560
commit e7783fe6cc
13 changed files with 127 additions and 17 deletions

View File

@@ -1,7 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -594,6 +594,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretReminderRecipients: z
.object({
user: UsersSchema.pick({ id: true, email: true, username: true }),
id: z.string()
})
.array(),
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),

View File

@@ -0,0 +1,8 @@
export type TSecretReminderRecipient = {
user: {
id: string;
username: string;
email?: string | null;
};
id: string;
};

View File

@@ -95,7 +95,17 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
`${TableName.SecretV2}.id`, `${TableName.SecretV2}.id`,
`${TableName.SecretRotationV2SecretMapping}.secretId` `${TableName.SecretRotationV2SecretMapping}.secretId`
) )
.leftJoin(
TableName.SecretReminderRecipients,
`${TableName.SecretV2}.id`,
`${TableName.SecretReminderRecipients}.secretId`
)
.leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`)
.select(selectAllTableCols(TableName.SecretV2)) .select(selectAllTableCols(TableName.SecretV2))
.select(db.ref("id").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientId"))
.select(db.ref("username").withSchema(TableName.Users).as("reminderRecipientUsername"))
.select(db.ref("email").withSchema(TableName.Users).as("reminderRecipientEmail"))
.select(db.ref("id").withSchema(TableName.Users).as("reminderRecipientUserId"))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
@@ -119,6 +129,23 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
slug, slug,
name: slug name: slug
}) })
},
{
key: "reminderRecipientId",
label: "secretReminderRecipients" as const,
mapper: ({
reminderRecipientId,
reminderRecipientUsername,
reminderRecipientEmail,
reminderRecipientUserId
}) => ({
user: {
id: reminderRecipientUserId,
username: reminderRecipientUsername,
email: reminderRecipientEmail
},
id: reminderRecipientId
})
} }
] ]
}); });
@@ -607,6 +634,12 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id` `${TableName.SecretTag}.id`
) )
.leftJoin(
TableName.SecretReminderRecipients,
`${TableName.SecretV2}.id`,
`${TableName.SecretReminderRecipients}.secretId`
)
.leftJoin(TableName.Users, `${TableName.SecretReminderRecipients}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.leftJoin( .leftJoin(
TableName.SecretRotationV2SecretMapping, TableName.SecretRotationV2SecretMapping,
@@ -635,6 +668,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
}) as rank` }) as rank`
) )
) )
.select(db.ref("id").withSchema(TableName.SecretReminderRecipients).as("reminderRecipientId"))
.select(db.ref("username").withSchema(TableName.Users).as("reminderRecipientUsername"))
.select(db.ref("email").withSchema(TableName.Users).as("reminderRecipientEmail"))
.select(db.ref("id").withSchema(TableName.Users).as("reminderRecipientUserId"))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
@@ -679,6 +716,23 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
isRotatedSecret: Boolean(el.rotationId) isRotatedSecret: Boolean(el.rotationId)
}), }),
childrenMapper: [ childrenMapper: [
{
key: "reminderRecipientId",
label: "secretReminderRecipients" as const,
mapper: ({
reminderRecipientId,
reminderRecipientUsername,
reminderRecipientEmail,
reminderRecipientUserId
}) => ({
user: {
id: reminderRecipientUserId,
username: reminderRecipientUsername,
email: reminderRecipientEmail
},
id: reminderRecipientId
})
},
{ {
key: "tagId", key: "tagId",
label: "tags" as const, label: "tags" as const,

View File

@@ -10,6 +10,7 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns"; import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretReminderRecipient } from "../secret-reminder-recipients/secret-reminder-recipients-types";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
@@ -668,6 +669,7 @@ export const reshapeBridgeSecret = (
secretMetadata?: ResourceMetadataDTO; secretMetadata?: ResourceMetadataDTO;
isRotatedSecret?: boolean; isRotatedSecret?: boolean;
rotationId?: string; rotationId?: string;
secretReminderRecipients?: TSecretReminderRecipient[];
}, },
secretValueHidden: boolean secretValueHidden: boolean
) => ({ ) => ({
@@ -699,6 +701,7 @@ export const reshapeBridgeSecret = (
updatedAt: secret.updatedAt, updatedAt: secret.updatedAt,
isRotatedSecret: secret.isRotatedSecret, isRotatedSecret: secret.isRotatedSecret,
rotationId: secret.rotationId, rotationId: secret.rotationId,
secretReminderRecipients: secret.secretReminderRecipients || [],
...(secretValueHidden ...(secretValueHidden
? { ? {
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK, secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,

View File

@@ -539,7 +539,8 @@ export const secretV2BridgeServiceFactory = ({
oldSecret: { oldSecret: {
id: secret.id, id: secret.id,
secretReminderNote: secret.reminderNote, secretReminderNote: secret.reminderNote,
secretReminderRepeatDays: secret.reminderRepeatDays secretReminderRepeatDays: secret.reminderRepeatDays,
secretReminderRecipients: secret.secretReminderRecipients?.map((el) => el.user.id)
}, },
projectId projectId
}); });

View File

@@ -396,6 +396,7 @@ export const decryptSecretRaw = (
id: secret.id, id: secret.id,
user: secret.userId, user: secret.userId,
tags: secret.tags?.map((el) => ({ ...el, name: el.slug })), tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
secretReminderRecipients: [],
skipMultilineEncoding: secret.skipMultilineEncoding, skipMultilineEncoding: secret.skipMultilineEncoding,
secretReminderRepeatDays: secret.secretReminderRepeatDays, secretReminderRepeatDays: secret.secretReminderRepeatDays,
secretReminderNote: secret.secretReminderNote, secretReminderNote: secret.secretReminderNote,

View File

@@ -259,6 +259,8 @@ export const secretQueueFactory = ({
}); });
} }
const existingRecipients = await secretReminderRecipientsDAL.findUsersBySecretId(newSecret.id);
if (recipients?.length) { if (recipients?.length) {
await secretReminderRecipientsDAL.transaction(async (tx) => { await secretReminderRecipientsDAL.transaction(async (tx) => {
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx); await secretReminderRecipientsDAL.delete({ secretId: newSecret.id }, tx);
@@ -271,6 +273,8 @@ export const secretQueueFactory = ({
tx tx
); );
}); });
} else if (existingRecipients) {
await secretReminderRecipientsDAL.delete({ secretId: newSecret.id });
} }
await queueService.queue( await queueService.queue(
@@ -306,12 +310,20 @@ export const secretQueueFactory = ({
}; };
const handleSecretReminder = async ({ newSecret, oldSecret, projectId }: THandleReminderDTO) => { const handleSecretReminder = async ({ newSecret, oldSecret, projectId }: THandleReminderDTO) => {
const { secretReminderRepeatDays, secretReminderNote } = newSecret; const { secretReminderRepeatDays, secretReminderNote, secretReminderRecipients } = newSecret;
const recipientsUpdated =
secretReminderRecipients?.some(
(newId) => !oldSecret.secretReminderRecipients?.find((oldId) => newId === oldId)
) ||
(secretReminderRecipients?.length !== oldSecret.secretReminderRecipients?.length &&
secretReminderRepeatDays !== null);
if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) { if (newSecret.type !== SecretType.Personal && secretReminderRepeatDays !== undefined) {
if ( if (
(secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) || (secretReminderRepeatDays && oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
(secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote) (secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote) ||
recipientsUpdated
) { ) {
await addSecretReminder({ await addSecretReminder({
oldSecret, oldSecret,

View File

@@ -685,6 +685,7 @@ export const secretServiceFactory = ({
...secret, ...secret,
workspace: projectId, workspace: projectId,
environment, environment,
secretReminderRecipients: [],
secretPath: groupedPaths[secret.folderId][0].path secretPath: groupedPaths[secret.folderId][0].path
})) }))
}; };

View File

@@ -80,6 +80,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
comment: el.secretComment || "", comment: el.secretComment || "",
reminderRepeatDays: el.secretReminderRepeatDays, reminderRepeatDays: el.secretReminderRepeatDays,
reminderNote: el.secretReminderNote, reminderNote: el.secretReminderNote,
secretReminderRecipients: el.secretReminderRecipients,
createdAt: el.createdAt, createdAt: el.createdAt,
updatedAt: el.updatedAt, updatedAt: el.updatedAt,
version: el.version, version: el.version,

View File

@@ -7,6 +7,14 @@ export enum SecretType {
Personal = "personal" Personal = "personal"
} }
export type SecretReminderRecipient = {
user: {
id: string;
username: string;
email: string;
};
id: string;
};
export type EncryptedSecret = { export type EncryptedSecret = {
id: string; id: string;
version: number; version: number;
@@ -56,6 +64,7 @@ export type SecretV3RawSanitized = {
secretMetadata?: { key: string; value: string }[]; secretMetadata?: { key: string; value: string }[];
isReminderEvent?: boolean; isReminderEvent?: boolean;
isRotatedSecret?: boolean; isRotatedSecret?: boolean;
secretReminderRecipients?: SecretReminderRecipient[];
rotationId?: string; rotationId?: string;
}; };
@@ -81,6 +90,7 @@ export type SecretV3Raw = {
updatedAt: string; updatedAt: string;
isRotatedSecret?: boolean; isRotatedSecret?: boolean;
rotationId?: string; rotationId?: string;
secretReminderRecipients?: SecretReminderRecipient[];
}; };
export type SecretV3RawResponse = { export type SecretV3RawResponse = {

View File

@@ -58,7 +58,6 @@ export const CreateReminderForm = ({
const { const {
register, register,
control, control,
reset,
setValue, setValue,
handleSubmit, handleSubmit,
formState: { isSubmitting } formState: { isSubmitting }
@@ -74,19 +73,10 @@ export const CreateReminderForm = ({
onOpenChange(false, data); onOpenChange(false, data);
}; };
useEffect(() => {
if (isOpen) {
reset({
days: repeatDays || undefined,
note: note || ""
});
}
}, [isOpen]);
useEffect(() => { useEffect(() => {
// On initial load, filter the members to only include the recipients // On initial load, filter the members to only include the recipients
if (members.length) { if (members.length) {
const filteredMembers = members.filter((m) => recipients?.includes(m.id)); const filteredMembers = members.filter((m) => recipients?.find((r) => r === m.user.id));
setValue( setValue(
"recipients", "recipients",
filteredMembers.map((m) => ({ filteredMembers.map((m) => ({
@@ -95,7 +85,7 @@ export const CreateReminderForm = ({
})) }))
); );
} }
}, [members, isOpen]); }, [members, isOpen, recipients]);
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
@@ -122,6 +112,7 @@ export const CreateReminderForm = ({
onChange={(el) => setValue("days", parseInt(el.target.value, 10))} onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
type="number" type="number"
placeholder="31" placeholder="31"
defaultValue={repeatDays || undefined}
value={field.value || undefined} value={field.value || undefined}
/> />
</FormControl> </FormControl>
@@ -144,6 +135,7 @@ export const CreateReminderForm = ({
placeholder="Remember to rotate the AWS secret every month." placeholder="Remember to rotate the AWS secret every month."
className="border border-mineshaft-600 text-sm" className="border border-mineshaft-600 text-sm"
rows={8} rows={8}
defaultValue={note || ""}
reSize="none" reSize="none"
cols={30} cols={30}
{...register("note")} {...register("note")}

View File

@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons"; import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
@@ -235,6 +236,16 @@ export const SecretDetailSidebar = ({
const secretReminderRepeatDays = watch("reminderRepeatDays"); const secretReminderRepeatDays = watch("reminderRepeatDays");
const secretReminderNote = watch("reminderNote"); const secretReminderNote = watch("reminderNote");
const secretReminderRecipients = watch("reminderRecipients"); const secretReminderRecipients = watch("reminderRecipients");
useEffect(() => {
setValue(
"reminderRecipients",
secret?.secretReminderRecipients?.map((el) => el.user.id),
{
shouldDirty: false
}
);
}, [secret?.secretReminderRecipients]);
const getModifiedByIcon = (userType: string | undefined | null) => { const getModifiedByIcon = (userType: string | undefined | null) => {
switch (userType) { switch (userType) {
case ActorType.USER: case ActorType.USER:

View File

@@ -49,6 +49,8 @@ export const SecretListView = ({
isProtectedBranch = false, isProtectedBranch = false,
importedBy importedBy
}: Props) => { }: Props) => {
console.log("secretssssss", secrets);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"deleteSecret", "deleteSecret",
@@ -184,6 +186,12 @@ export const SecretListView = ({
const tagIds = tags?.map(({ id }) => id); const tagIds = tags?.map(({ id }) => id);
const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id); const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id);
const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds); const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds);
const isSameRecipients =
!reminderRecipients?.some(
(newId) => !orgSecret.secretReminderRecipients?.find((oldId) => newId === oldId.user.id)
) && reminderRecipients?.length === orgSecret.secretReminderRecipients?.length;
const isSharedSecUnchanged = const isSharedSecUnchanged =
( (
[ [
@@ -196,7 +204,9 @@ export const SecretListView = ({
"reminderRecipients", "reminderRecipients",
"secretMetadata" "secretMetadata"
] as const ] as const
).every((el) => orgSecret[el] === modSecret[el]) && isSameTags; ).every((el) => orgSecret[el] === modSecret[el]) &&
isSameTags &&
isSameRecipients;
try { try {
// personal secret change // personal secret change