feat(secret-syncs): better permissioning

This commit is contained in:
Daniel Hougaard
2025-06-18 00:17:39 +04:00
parent e61d35d824
commit 3f654e115d
7 changed files with 381 additions and 42 deletions
@@ -291,6 +291,13 @@ export const projectRoleFormSchema = z.object({
})
.array()
.default([]),
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Groups]: GroupPolicyActionSchema.array().default([]),
@@ -342,7 +349,6 @@ export const projectRoleFormSchema = z.object({
.default([]),
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretScanningDataSources]:
SecretScanningDataSourcePolicyActionSchema.array().default([]),
@@ -366,7 +372,8 @@ type TConditionalFields =
| ProjectPermissionSub.CertificateTemplates
| ProjectPermissionSub.SshHosts
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.Identity;
| ProjectPermissionSub.Identity
| ProjectPermissionSub.SecretSyncs;
export const isConditionalSubjects = (
subject: ProjectPermissionSub
@@ -379,7 +386,8 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.SshHosts ||
subject === ProjectPermissionSub.SecretRotation ||
subject === ProjectPermissionSub.PkiSubscribers ||
subject === ProjectPermissionSub.CertificateTemplates;
subject === ProjectPermissionSub.CertificateTemplates ||
subject === ProjectPermissionSub.SecretSyncs;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema> = [];
@@ -484,7 +492,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshHostGroups
ProjectPermissionSub.SshHostGroups,
ProjectPermissionSub.SecretSyncs
].includes(subject)
) {
// from above statement we are sure it won't be undefined
@@ -786,7 +795,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const canImportSecrets = action.includes(ProjectPermissionSecretSyncActions.ImportSecrets);
const canRemoveSecrets = action.includes(ProjectPermissionSecretSyncActions.RemoveSecrets);
if (!formVal[subject]) formVal[subject] = [{}];
if (!formVal[subject]) formVal[subject] = [{ conditions: [], inverted: false }];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0][ProjectPermissionSecretSyncActions.Read] = true;
@@ -35,6 +35,7 @@ import {
TFormSchema
} from "./ProjectRoleModifySection.utils";
import { SecretPermissionConditions } from "./SecretPermissionConditions";
import { SecretSyncPermissionConditions } from "./SecretSyncPermissionConditions";
import { SshHostPermissionConditions } from "./SshHostPermissionConditions";
type Props = {
@@ -69,6 +70,10 @@ export const renderConditionalComponents = (
return <PkiTemplatePermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.SecretSyncs) {
return <SecretSyncPermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
}
@@ -0,0 +1,186 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const SecretSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.message ||
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.root?.message;
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
<p className="text-sm text-mineshaft-400">
Conditions determine when a policy will be applied (always if no conditions are present).
</p>
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition = watch(
`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}`
) as {
lhs: string;
rhs: string;
operator: string;
};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{conditionErrorMessage}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
);
};