Merge pull request #3810 from Infisical/daniel/secret-syncs-permissions

feat(secret-syncs): better permissioning
This commit is contained in:
Maidul Islam
2025-06-17 21:44:47 -04:00
committed by GitHub
14 changed files with 521 additions and 101 deletions

View File

@@ -211,6 +211,11 @@ export type SecretFolderSubjectFields = {
secretPath: string;
};
export type SecretSyncSubjectFields = {
environment: string;
secretPath: string;
};
export type DynamicSecretSubjectFields = {
environment: string;
secretPath: string;
@@ -267,6 +272,10 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
)
]
| [
ProjectPermissionSecretSyncActions,
ProjectPermissionSub.SecretSyncs | (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
]
| [
ProjectPermissionActions,
(
@@ -323,7 +332,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
@@ -412,6 +420,23 @@ const DynamicSecretConditionV2Schema = z
})
.partial();
const SecretSyncConditionV2Schema = z
.object({
environment: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
]),
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA
})
.partial();
const SecretImportConditionSchema = z
.object({
environment: z.union([
@@ -671,12 +696,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
@@ -836,6 +855,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
),
conditions: SecretSyncConditionV2Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
...GeneralPermissionSchema
]);

View File

@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -89,7 +89,17 @@ export const secretSyncServiceFactory = ({
projectId
});
return secretSyncs as TSecretSync[];
return secretSyncs.filter((secretSync) =>
permission.can(
ProjectPermissionSecretSyncActions.Read,
secretSync.environment && secretSync.folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
: ProjectPermissionSub.SecretSyncs
)
) as TSecretSync[];
};
const listSecretSyncsBySecretPath = async (
@@ -105,7 +115,15 @@ export const secretSyncServiceFactory = ({
projectId
});
if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) {
if (
permission.cannot(
ProjectPermissionSecretSyncActions.Read,
subject(ProjectPermissionSub.SecretSyncs, {
environment,
secretPath
})
)
) {
return [];
}
@@ -142,7 +160,12 @@ export const secretSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSub.SecretSyncs
secretSync.environment && secretSync.folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
: ProjectPermissionSub.SecretSyncs
);
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
@@ -179,7 +202,12 @@ export const secretSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSub.SecretSyncs
secretSync.environment && secretSync.folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
: ProjectPermissionSub.SecretSyncs
);
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
@@ -217,13 +245,17 @@ export const secretSyncServiceFactory = ({
ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSub.SecretSyncs
subject(ProjectPermissionSub.SecretSyncs, { environment, secretPath })
);
throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, {
environment,
secretPath
});
throwIfMissingSecretReadValueOrDescribePermission(
projectPermission,
ProjectPermissionSecretActions.DescribeSecret,
{
environment,
secretPath
}
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
@@ -286,10 +318,38 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSub.SecretSyncs
);
// we always check the permission against the existing environment / secret path
// if no secret path / environment is present on the secret sync, we need to check without conditions
if (secretSync.environment?.slug && secretSync.folder?.path) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Edit,
subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSub.SecretSyncs
);
}
// if the user is updating the secret path or environment, we need to check the permission against the new values
if (secretPath || environment) {
const environmentToCheck = environment || secretSync.environment?.slug || "";
const secretPathToCheck = secretPath || secretSync.folder?.path || "";
if (environmentToCheck && secretPathToCheck) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Edit,
subject(ProjectPermissionSub.SecretSyncs, {
environment: environmentToCheck,
secretPath: secretPathToCheck
})
);
}
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
@@ -315,7 +375,7 @@ export const secretSyncServiceFactory = ({
if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: updatedEnvironment,
secretPath: updatedSecretPath
});
@@ -374,10 +434,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSub.SecretSyncs
);
if (secretSync.environment?.slug && secretSync.folder?.path) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Delete,
subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSub.SecretSyncs
);
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
@@ -441,10 +511,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSub.SecretSyncs
);
if (secretSync.environment?.slug && secretSync.folder?.path) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.SyncSecrets,
subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSub.SecretSyncs
);
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
@@ -503,10 +583,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSub.SecretSyncs
);
if (secretSync.environment?.slug && secretSync.folder?.path) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.ImportSecrets,
subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSub.SecretSyncs
);
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({
@@ -559,10 +649,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.RemoveSecrets,
ProjectPermissionSub.SecretSyncs
);
if (secretSync.environment?.slug && secretSync.folder?.path) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.RemoveSecrets,
subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.RemoveSecrets,
ProjectPermissionSub.SecretSyncs
);
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({

View File

@@ -12,7 +12,7 @@ Each permission consists of:
- **Subject**: The resource the permission applies to (e.g., secrets, members, settings)
- **Action**: The operation that can be performed (e.g., read, create, edit, delete)
Some project-level resources—specifically `secrets`, `secret-folders`, `secret-imports`, and `dynamic-secrets`support conditional permissions and permission inversion for more granular access control. Conditions allow you to specify criteria (like environment, secret path, or tags) that must be met for the permission to apply.
Some project-level resources—specifically `secrets`, `secret-folders`, `secret-imports`, `dynamic-secrets`, and `secret-syncs`, support conditional permissions and permission inversion for more granular access control. Conditions allow you to specify criteria (like environment, secret path, or tags) that must be met for the permission to apply.
## Available Project Permissions
@@ -208,6 +208,8 @@ Supports conditions and permission inversion
#### Subject: `secret-syncs`
Supports conditions and permission inversion.
| Action | Description |
| ---------------- | -------------------------------------------------- |
| `read` | View secret synchronization configurations |

View File

@@ -1,17 +1,45 @@
import { useEffect } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { subject } from "@casl/ability";
import { FilterableSelect, FormControl } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useWorkspace } from "@app/context";
import { useProjectPermission, useWorkspace } from "@app/context";
import {
ProjectPermissionSecretSyncActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { TSecretSyncForm } from "./schemas";
export const SecretSyncSourceFields = () => {
const { control, watch } = useFormContext<TSecretSyncForm>();
const { control, watch, setError, clearErrors } = useFormContext<TSecretSyncForm>();
const { permission } = useProjectPermission();
const { currentWorkspace } = useWorkspace();
const selectedEnvironment = watch("environment");
const selectedSecretPath = watch("secretPath");
useEffect(() => {
const hasAccessToSource =
selectedEnvironment &&
permission.can(
ProjectPermissionSecretSyncActions.Create,
subject(ProjectPermissionSub.SecretSyncs, {
environment: selectedEnvironment.slug,
secretPath: selectedSecretPath
})
);
if (!hasAccessToSource) {
setError("secretPath", {
message: "You do not have permission to create secret syncs in this environment or path."
});
} else {
clearErrors("secretPath");
}
}, [selectedEnvironment, selectedSecretPath]);
return (
<>

View File

@@ -263,6 +263,11 @@ export type SecretImportSubjectFields = {
secretPath: string;
};
export type SecretSyncSubjectFields = {
environment: string;
secretPath: string;
};
export type SecretRotationSubjectFields = {
environment: string;
secretPath: string;
@@ -303,6 +308,13 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
)
]
| [
ProjectPermissionSecretSyncActions,
(
| ProjectPermissionSub.SecretSyncs
| (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
)
]
| [
ProjectPermissionActions,
(
@@ -365,7 +377,6 @@ export type ProjectPermissionSet =
]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]

View File

@@ -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
@@ -515,6 +524,36 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return;
}
if (subject === ProjectPermissionSub.SecretSyncs) {
const canRead = action.includes(ProjectPermissionSecretSyncActions.Read);
const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit);
const canDelete = action.includes(ProjectPermissionSecretSyncActions.Delete);
const canCreate = action.includes(ProjectPermissionSecretSyncActions.Create);
const canSyncSecrets = action.includes(ProjectPermissionSecretSyncActions.SyncSecrets);
const canImportSecrets = action.includes(
ProjectPermissionSecretSyncActions.ImportSecrets
);
const canRemoveSecrets = action.includes(
ProjectPermissionSecretSyncActions.RemoveSecrets
);
if (!formVal[subject]) formVal[subject] = [{ conditions: [], inverted: false }];
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
[ProjectPermissionSecretSyncActions.Read]: canRead,
[ProjectPermissionSecretSyncActions.Create]: canCreate,
[ProjectPermissionSecretSyncActions.Edit]: canEdit,
[ProjectPermissionSecretSyncActions.Delete]: canDelete,
[ProjectPermissionSecretSyncActions.SyncSecrets]: canSyncSecrets,
[ProjectPermissionSecretSyncActions.ImportSecrets]: canImportSecrets,
[ProjectPermissionSecretSyncActions.RemoveSecrets]: canRemoveSecrets,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return;
}
if (subject === ProjectPermissionSub.DynamicSecrets) {
const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential);
const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential);
@@ -777,31 +816,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return;
}
if (subject === ProjectPermissionSub.SecretSyncs) {
const canRead = action.includes(ProjectPermissionSecretSyncActions.Read);
const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit);
const canDelete = action.includes(ProjectPermissionSecretSyncActions.Delete);
const canCreate = action.includes(ProjectPermissionSecretSyncActions.Create);
const canSyncSecrets = action.includes(ProjectPermissionSecretSyncActions.SyncSecrets);
const canImportSecrets = action.includes(ProjectPermissionSecretSyncActions.ImportSecrets);
const canRemoveSecrets = action.includes(ProjectPermissionSecretSyncActions.RemoveSecrets);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0][ProjectPermissionSecretSyncActions.Read] = true;
if (canEdit) formVal[subject]![0][ProjectPermissionSecretSyncActions.Edit] = true;
if (canCreate) formVal[subject]![0][ProjectPermissionSecretSyncActions.Create] = true;
if (canDelete) formVal[subject]![0][ProjectPermissionSecretSyncActions.Delete] = true;
if (canSyncSecrets)
formVal[subject]![0][ProjectPermissionSecretSyncActions.SyncSecrets] = true;
if (canImportSecrets)
formVal[subject]![0][ProjectPermissionSecretSyncActions.ImportSecrets] = true;
if (canRemoveSecrets)
formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true;
return;
}
if (subject === ProjectPermissionSub.SecretScanningDataSources) {
const canRead = action.includes(ProjectPermissionSecretScanningDataSourceActions.Read);
const canEdit = action.includes(ProjectPermissionSecretScanningDataSourceActions.Edit);

View File

@@ -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} />;
}

View File

@@ -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="remove"
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>
);
};

View File

@@ -1,4 +1,5 @@
import { useCallback, useMemo } from "react";
import { subject } from "@casl/ability";
import {
faBan,
faCalendarCheck,
@@ -117,6 +118,14 @@ export const SecretSyncRow = ({
const destinationDetails = SECRET_SYNC_MAP[destination];
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return (
<Tr
onClick={() =>
@@ -264,7 +273,7 @@ export const SecretSyncRow = ({
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.SyncSecrets}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
@@ -295,7 +304,7 @@ export const SecretSyncRow = ({
{syncOption?.canImportSecrets && (
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.ImportSecrets}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
@@ -326,7 +335,7 @@ export const SecretSyncRow = ({
)}
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.RemoveSecrets}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
@@ -356,7 +365,7 @@ export const SecretSyncRow = ({
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
@@ -373,7 +382,7 @@ export const SecretSyncRow = ({
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Delete}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem

View File

@@ -1,4 +1,5 @@
import { useCallback } from "react";
import { subject } from "@casl/ability";
import {
faBan,
faCheck,
@@ -63,7 +64,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
const triggerSyncSecrets = useTriggerSecretSyncSyncSecrets();
const updateSync = useUpdateSecretSync();
const { destination } = secretSync;
const { destination, environment, folder } = secretSync;
const destinationName = SECRET_SYNC_MAP[destination].name;
const { syncOption } = useSecretSyncOption(destination);
@@ -128,6 +129,14 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
}
};
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return (
<>
<div className="ml-auto mt-4 flex flex-wrap items-center justify-end gap-2">
@@ -157,7 +166,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
<div>
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.SyncSecrets}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<Button
@@ -194,7 +203,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
{syncOption?.canImportSecrets && (
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.ImportSecrets}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
@@ -222,7 +231,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
)}
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.RemoveSecrets}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
@@ -249,7 +258,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
@@ -267,7 +276,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Delete}
a={ProjectPermissionSub.SecretSyncs}
a={permissionSubject}
>
{(isAllowed: boolean) => (
<DropdownMenuItem

View File

@@ -1,4 +1,5 @@
import { ReactNode } from "react";
import { subject } from "@casl/ability";
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -35,7 +36,7 @@ type Props = {
};
export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: Props) => {
const { destination, connection } = secretSync;
const { destination, connection, folder, environment } = secretSync;
const app = APP_CONNECTION_MAP[connection.app].name;
@@ -101,14 +102,19 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }:
throw new Error(`Unhandled Destination Section components: ${destination}`);
}
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Destination Configuration</h3>
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"

View File

@@ -1,4 +1,5 @@
import { useMemo } from "react";
import { subject } from "@casl/ability";
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
@@ -16,7 +17,8 @@ type Props = {
};
export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) => {
const { syncStatus, lastSyncMessage, lastSyncedAt, name, description } = secretSync;
const { syncStatus, lastSyncMessage, lastSyncedAt, name, description, environment, folder } =
secretSync;
const failureMessage = useMemo(() => {
if (syncStatus === SecretSyncStatus.Failed) {
@@ -32,14 +34,19 @@ export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) =
return null;
}, [syncStatus, lastSyncMessage]);
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return (
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Details</h3>
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"

View File

@@ -1,4 +1,5 @@
import { ReactNode } from "react";
import { subject } from "@casl/ability";
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -21,7 +22,9 @@ type Props = {
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
const {
destination,
syncOptions: { initialSyncBehavior, disableSecretDeletion, keySchema }
syncOptions: { initialSyncBehavior, disableSecretDeletion, keySchema },
environment,
folder
} = secretSync;
let AdditionalSyncOptionsComponent: ReactNode;
@@ -59,15 +62,20 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return (
<div>
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"

View File

@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faEdit, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -16,6 +17,14 @@ type Props = {
export const SecretSyncSourceSection = ({ secretSync, onEditSource }: Props) => {
const { folder, environment } = secretSync;
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return (
<div>
<div className="flex w-full flex-col gap-3 rounded-lg border border-mineshaft-600 bg-mineshaft-900 px-4 py-3">
@@ -35,10 +44,7 @@ export const SecretSyncSourceSection = ({ secretSync, onEditSource }: Props) =>
</div>
</Tooltip>
)}
<ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
<ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
{(isAllowed) => (
<IconButton
variant="plain"