Merge branch 'main' into feat/improveSecretReferenceWarning

This commit is contained in:
carlosmonastyrski
2025-04-29 08:43:35 -03:00
376 changed files with 9209 additions and 1020 deletions
@@ -9,6 +9,7 @@ import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol";
import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol";
import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol";
import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol";
import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol";
import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol";
import { VercelSyncDestinationCol } from "./VercelSyncDestinationCol";
import { WindmillSyncDestinationCol } from "./WindmillSyncDestinationCol";
@@ -43,6 +44,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <VercelSyncDestinationCol secretSync={secretSync} />;
case SecretSync.Windmill:
return <WindmillSyncDestinationCol secretSync={secretSync} />;
case SecretSync.TeamCity:
return <TeamCitySyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`
@@ -0,0 +1,14 @@
import { TTeamCitySync } from "@app/hooks/api/secretSyncs/types/teamcity-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TTeamCitySync;
};
export const TeamCitySyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};
@@ -94,6 +94,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => {
primaryText = destinationConfig.workspace;
secondaryText = destinationConfig.path;
break;
case SecretSync.TeamCity:
primaryText = destinationConfig.project;
secondaryText = destinationConfig.buildConfig;
break;
default:
throw new Error(`Unhandled Destination Col Values ${destination}`);
}
@@ -135,7 +135,6 @@ export const SecretOverviewSecretRotationRow = ({
<img
src={`/images/integrations/${image}`}
style={{
height: "11px",
width: "11px"
}}
alt={`${rotationType} logo`}
@@ -4,6 +4,7 @@ import { subject } from "@casl/ability";
import {
faCheck,
faCopy,
faEyeSlash,
faProjectDiagram,
faTrash,
faXmark
@@ -25,7 +26,6 @@ import {
ModalTrigger,
Tooltip
} from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
@@ -149,7 +149,13 @@ export const SecretEditRow = ({
);
}
}
reset({ value });
if (secretValueHidden && !isOverride) {
setTimeout(() => {
reset({ value: defaultValue || null });
}, 50);
} else {
reset({ value });
}
};
const handleEditSecret = async ({ secretValue }: { secretValue: string }) => {
@@ -169,6 +175,16 @@ export const SecretEditRow = ({
ProjectPermissionSecretActions.ReadValue
);
const canEditSecretValue = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: ["*"]
})
);
const handleDeleteSecret = useCallback(async () => {
setIsDeleting.on();
setIsModalOpen(false);
@@ -190,29 +206,32 @@ export const SecretEditRow = ({
deleteKey={secretName}
onDeleteApproved={handleDeleteSecret}
/>
{secretValueHidden && !isOverride && (
<Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
>
<FontAwesomeIcon className="pl-2" size="sm" icon={faEyeSlash} />
</Tooltip>
)}
<div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2">
{secretValueHidden ? (
<Blur tooltipText="You do not have permission to read the value of this secret." />
) : (
<Controller
disabled={isImportedSecret && !defaultValue}
control={control}
name="value"
render={({ field }) => (
<InfisicalSecretInput
{...field}
isReadOnly={isImportedSecret || isRotatedSecret}
value={field.value as string}
key="secret-input"
isVisible={isVisible}
secretPath={secretPath}
environment={environment}
isImport={isImportedSecret}
/>
)}
/>
)}
<Controller
disabled={isImportedSecret && !defaultValue}
control={control}
name="value"
render={({ field }) => (
<InfisicalSecretInput
{...field}
isReadOnly={isImportedSecret || (isRotatedSecret && !isOverride)}
value={field.value as string}
key="secret-input"
isVisible={isVisible && !secretValueHidden}
secretPath={secretPath}
environment={environment}
isImport={isImportedSecret}
defaultValue={secretValueHidden ? "" : undefined}
/>
)}
/>
</div>
<div
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faCircle } from "@fortawesome/free-regular-svg-icons";
import {
faAngleDown,
@@ -14,6 +15,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2";
import { useProjectPermission } from "@app/context";
import {
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WorkspaceEnv } from "@app/hooks/api/types";
@@ -73,6 +79,28 @@ export const SecretOverviewTableRow = ({
const totalCols = environments.length + 1; // secret key row
const [isSecretVisible, setIsSecretVisible] = useToggle();
const { permission } = useProjectPermission();
const getDefaultValue = (
secret: SecretV3RawSanitized | undefined,
importedSecret: { secret?: SecretV3RawSanitized } | undefined
) => {
const canEditSecretValue = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: secret?.env || "",
secretPath: secret?.path || "",
secretName: secret?.key || "",
secretTags: ["*"]
})
);
if (secret?.secretValueHidden && !secret?.valueOverride) {
return canEditSecretValue ? "******" : "";
}
return secret?.valueOverride || secret?.value || importedSecret?.secret?.value || "";
};
return (
<>
<Tr isHoverable isSelectable onClick={() => setIsFormExpanded.toggle()} className="group">
@@ -237,13 +265,7 @@ export const SecretOverviewTableRow = ({
isVisible={isSecretVisible}
secretName={secretKey}
secretValueHidden={secret?.secretValueHidden || false}
defaultValue={
secret?.secretValueHidden
? ""
: secret?.valueOverride ||
secret?.value ||
importedSecret?.secret?.value
}
defaultValue={getDefaultValue(secret, importedSecret)}
secretId={secret?.id}
isOverride={Boolean(secret?.valueOverride)}
isImportedSecret={isImportedSecret}
@@ -25,7 +25,7 @@ import {
} from "@app/components/v2";
import { Badge } from "@app/components/v2/Badge";
import {
ProjectPermissionActions,
ProjectPermissionMemberActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
@@ -289,7 +289,10 @@ export const AccessApprovalRequest = ({
))}
</DropdownMenuContent>
</DropdownMenu>
{!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && (
{!!permission.can(
ProjectPermissionMemberActions.Read,
ProjectPermissionSub.Member
) && (
<DropdownMenu>
<DropdownMenuTrigger>
<Button
@@ -22,7 +22,7 @@ import {
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionActions,
ProjectPermissionMemberActions,
ProjectPermissionSub,
useProjectPermission,
useUser,
@@ -171,7 +171,10 @@ export const SecretApprovalRequest = () => {
))}
</DropdownMenuContent>
</DropdownMenu>
{!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && (
{!!permission.can(
ProjectPermissionMemberActions.Read,
ProjectPermissionSub.Member
) && (
<DropdownMenu>
<DropdownMenuTrigger>
<Button
@@ -6,10 +6,28 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
import {
Button,
FilterableSelect,
FormControl,
Input,
Modal,
ModalContent,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetWorkspaceUsers } from "@app/hooks/api";
const ReminderFormSchema = z.object({
note: z.string().optional().nullable(),
recipients: z
.array(
z.object({
label: z.string(),
value: z.string().uuid()
})
)
.optional(),
days: z
.number()
.min(1, { message: "Must be at least 1 day" })
@@ -22,6 +40,7 @@ interface ReminderFormProps {
isOpen: boolean;
repeatDays?: number | null;
note?: string | null;
recipients?: string[] | null;
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
}
@@ -29,12 +48,16 @@ export const CreateReminderForm = ({
isOpen,
onOpenChange,
repeatDays,
note
note,
recipients
}: ReminderFormProps) => {
const { currentWorkspace } = useWorkspace();
const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace?.id);
const {
register,
control,
reset,
setValue,
handleSubmit,
formState: { isSubmitting }
@@ -51,13 +74,23 @@ export const CreateReminderForm = ({
};
useEffect(() => {
if (isOpen) {
reset({
days: repeatDays || undefined,
note: note || ""
});
// On initial load, filter the members to only include the recipients
if (members.length) {
const filteredMembers = members.filter((m) => recipients?.find((r) => r === m.user.id));
setValue(
"recipients",
filteredMembers.map((m) => ({
label: m.user.username || m.user.email,
value: m.user.id
}))
);
}
}, [isOpen]);
}, [members, isOpen, recipients]);
useEffect(() => {
if (repeatDays) setValue("days", repeatDays);
if (note) setValue("note", note);
}, [repeatDays, note]);
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
@@ -74,6 +107,7 @@ export const CreateReminderForm = ({
render={({ field, fieldState }) => (
<>
<FormControl
isRequired
className="mb-0"
label="Reminder Interval (in days)"
isError={Boolean(fieldState.error)}
@@ -83,6 +117,7 @@ export const CreateReminderForm = ({
onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
type="number"
placeholder="31"
defaultValue={repeatDays || undefined}
value={field.value || undefined}
/>
</FormControl>
@@ -105,11 +140,43 @@ export const CreateReminderForm = ({
placeholder="Remember to rotate the AWS secret every month."
className="border border-mineshaft-600 text-sm"
rows={8}
defaultValue={note || ""}
reSize="none"
cols={30}
{...register("note")}
/>
</FormControl>
<Controller
control={control}
name="recipients"
render={({ field }) => (
<FormControl
tooltipText={
<div>
Select users to receive reminders.
<br />
<br /> If none are selected, all project members will receive the reminder.
</div>
}
label="Recipients"
className="mb-0"
>
<FilterableSelect
className="w-full"
placeholder="Select reminder recipients..."
isMulti
name="recipients"
options={members.map((member) => ({
label: member.user.username || member.user.email,
value: member.user.id
}))}
value={field.value}
onChange={field.onChange}
/>
</FormControl>
)}
/>
</div>
<div className="mt-7 flex items-center space-x-4">
<Button
@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
@@ -220,11 +221,12 @@ export const SecretDetailSidebar = ({
const handleReminderSubmit = async (
reminderRepeatDays: number | null | undefined,
reminderNote: string | null | undefined
reminderNote: string | null | undefined,
reminderRecipients: string[] | undefined
) => {
await onSaveSecret(
secret,
{ ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true },
{ ...secret, reminderRepeatDays, reminderNote, isReminderEvent: true, reminderRecipients },
() => {}
);
};
@@ -233,6 +235,16 @@ export const SecretDetailSidebar = ({
const secretReminderRepeatDays = watch("reminderRepeatDays");
const secretReminderNote = watch("reminderNote");
const secretReminderRecipients = watch("reminderRecipients");
useEffect(() => {
setValue(
"reminderRecipients",
secret?.secretReminderRecipients?.map((el) => el.user.id),
{
shouldDirty: false
}
);
}, [secret?.secretReminderRecipients]);
const getModifiedByIcon = (userType: string | undefined | null) => {
switch (userType) {
@@ -294,14 +306,20 @@ export const SecretDetailSidebar = ({
<CreateReminderForm
repeatDays={secretReminderRepeatDays}
note={secretReminderNote}
recipients={secretReminderRecipients}
isOpen={createReminderFormOpen}
onOpenChange={(_, data) => {
setCreateReminderFormOpen.toggle();
if (data) {
const recipients = data.recipients?.length
? data.recipients.map((recipient) => recipient.value)
: undefined;
setValue("reminderRepeatDays", data.days, { shouldDirty: false });
setValue("reminderNote", data.note, { shouldDirty: false });
handleReminderSubmit(data.days, data.note);
setValue("reminderRecipients", recipients, { shouldDirty: false });
handleReminderSubmit(data.days, data.note, recipients);
}
}}
/>
@@ -47,10 +47,9 @@ import {
} from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { Blur } from "@app/components/v2/Blur";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
import {
FontAwesomeSpriteName,
formSchema,
@@ -59,6 +58,8 @@ import {
} from "./SecretListView.utils";
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
const hiddenValue = "******";
type Props = {
secret: SecretV3RawSanitized;
onSaveSecret: (
@@ -109,6 +110,23 @@ export const SecretItem = memo(
const { permission } = useProjectPermission();
const { isRotatedSecret } = secret;
const canEditSecretValue = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: ["*"]
})
);
const getDefaultValue = () => {
if (secret.secretValueHidden) {
return canEditSecretValue ? hiddenValue : "";
}
return secret.valueOverride || secret.value || "";
};
const {
handleSubmit,
control,
@@ -122,11 +140,11 @@ export const SecretItem = memo(
} = useForm<TFormSchema>({
defaultValues: {
...secret,
value: secret.secretValueHidden ? "" : secret.value
value: getDefaultValue()
},
values: {
...secret,
value: secret.secretValueHidden ? "" : secret.value
value: getDefaultValue()
},
resolver: zodResolver(formSchema)
});
@@ -168,6 +186,7 @@ export const SecretItem = memo(
secretTags: selectedTagSlugs
})
);
const { secretValueHidden } = secret;
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
@@ -316,6 +335,13 @@ export const SecretItem = memo(
tabIndex={0}
role="button"
>
{secretValueHidden && !isOverriden && (
<Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
>
<FontAwesomeIcon className="pr-2" size="sm" icon={faEyeSlash} />
</Tooltip>
)}
{isOverriden ? (
<Controller
name="valueOverride"
@@ -331,8 +357,6 @@ export const SecretItem = memo(
/>
)}
/>
) : secretValueHidden ? (
<Blur tooltipText="You do not have permission to read the value of this secret." />
) : (
<Controller
name="value"
@@ -342,11 +366,11 @@ export const SecretItem = memo(
<InfisicalSecretInput
isReadOnly={isReadOnly || isRotatedSecret}
key="secret-value"
isVisible={isVisible}
isVisible={isVisible && !secretValueHidden}
environment={environment}
secretPath={secretPath}
{...field}
defaultValue={secretValueHidden ? "" : undefined}
defaultValue={secretValueHidden ? hiddenValue : undefined}
containerClassName="py-1.5 rounded-md transition-all"
/>
)}
@@ -87,6 +87,7 @@ export const SecretListView = ({
comment,
reminderRepeatDays,
reminderNote,
reminderRecipients,
tags,
skipMultilineEncoding,
newKey,
@@ -98,6 +99,7 @@ export const SecretListView = ({
comment: string;
reminderRepeatDays: number | null;
reminderNote: string | null;
reminderRecipients?: string[] | null;
tags: string[];
skipMultilineEncoding: boolean;
newKey: string;
@@ -133,6 +135,7 @@ export const SecretListView = ({
secretComment: comment,
secretReminderRepeatDays: reminderRepeatDays,
secretReminderNote: reminderNote,
secretReminderRecipients: reminderRecipients,
skipMultilineEncoding,
secretMetadata
});
@@ -174,6 +177,7 @@ export const SecretListView = ({
comment,
reminderRepeatDays,
reminderNote,
reminderRecipients,
secretMetadata,
isReminderEvent
} = modSecret;
@@ -182,6 +186,12 @@ export const SecretListView = ({
const tagIds = tags?.map(({ id }) => id);
const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id);
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 =
(
[
@@ -191,9 +201,12 @@ export const SecretListView = ({
"skipMultilineEncoding",
"reminderRepeatDays",
"reminderNote",
"reminderRecipients",
"secretMetadata"
] as const
).every((el) => orgSecret[el] === modSecret[el]) && isSameTags;
).every((el) => orgSecret[el] === modSecret[el]) &&
isSameTags &&
isSameRecipients;
try {
// personal secret change
@@ -226,6 +239,7 @@ export const SecretListView = ({
comment,
reminderRepeatDays,
reminderNote,
reminderRecipients,
secretId: orgSecret.id,
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding,
@@ -47,6 +47,7 @@ export const formSchema = z.object({
.nullable()
.optional(),
reminderNote: z.string().trim().nullable().optional(),
reminderRecipients: z.array(z.string().uuid()).optional(),
secretMetadata: z
.object({
key: z.string().trim().min(1),
@@ -55,7 +55,6 @@ export const SecretRotationItem = ({
<img
src={`/images/integrations/${image}`}
style={{
height: "11px",
width: "11px"
}}
alt={`${rotationType} logo`}
@@ -19,6 +19,7 @@ import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSec
import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection";
import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection";
import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection";
import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection";
import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection";
import { VercelSyncDestinationSection } from "./VercelSyncDestinationSection";
import { WindmillSyncDestinationSection } from "./WindmillSyncDestinationSection";
@@ -73,6 +74,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
case SecretSync.Windmill:
DestinationComponents = <WindmillSyncDestinationSection secretSync={secretSync} />;
break;
case SecretSync.TeamCity:
DestinationComponents = <TeamCitySyncDestinationSection secretSync={secretSync} />;
break;
default:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
@@ -0,0 +1,19 @@
import { GenericFieldLabel } from "@app/components/secret-syncs";
import { TTeamCitySync } from "@app/hooks/api/secretSyncs/types/teamcity-sync";
type Props = {
secretSync: TTeamCitySync;
};
export const TeamCitySyncDestinationSection = ({ secretSync }: Props) => {
const {
destinationConfig: { project, buildConfig }
} = secretSync;
return (
<>
<GenericFieldLabel label="Project">{project}</GenericFieldLabel>
<GenericFieldLabel label="Build Configuration">{buildConfig}</GenericFieldLabel>
</>
);
};
@@ -52,6 +52,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
case SecretSync.Camunda:
case SecretSync.Vercel:
case SecretSync.Windmill:
case SecretSync.TeamCity:
AdditionalSyncOptionsComponent = null;
break;
default: