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
@@ -211,6 +211,11 @@ export type SecretFolderSubjectFields = {
secretPath: string; secretPath: string;
}; };
export type SecretSyncSubjectFields = {
environment: string;
secretPath: string;
};
export type DynamicSecretSubjectFields = { export type DynamicSecretSubjectFields = {
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -267,6 +272,10 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields) | (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
) )
] ]
| [
ProjectPermissionSecretSyncActions,
ProjectPermissionSub.SecretSyncs | (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
]
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
( (
@@ -323,7 +332,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip] | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
@@ -412,6 +420,23 @@ const DynamicSecretConditionV2Schema = z
}) })
.partial(); .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 const SecretImportConditionSchema = z
.object({ .object({
environment: z.union([ environment: z.union([
@@ -671,12 +696,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take." "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({ z.object({
subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe( 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." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).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 ...GeneralPermissionSchema
]); ]);
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -89,7 +89,17 @@ export const secretSyncServiceFactory = ({
projectId 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 ( const listSecretSyncsBySecretPath = async (
@@ -105,7 +115,15 @@ export const secretSyncServiceFactory = ({
projectId projectId
}); });
if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) { if (
permission.cannot(
ProjectPermissionSecretSyncActions.Read,
subject(ProjectPermissionSub.SecretSyncs, {
environment,
secretPath
})
)
) {
return []; return [];
} }
@@ -142,7 +160,12 @@ export const secretSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Read, 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]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
@@ -179,7 +202,12 @@ export const secretSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Read, 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]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
@@ -217,13 +245,17 @@ export const secretSyncServiceFactory = ({
ForbiddenError.from(projectPermission).throwUnlessCan( ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Create, ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSub.SecretSyncs subject(ProjectPermissionSub.SecretSyncs, { environment, secretPath })
); );
throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, { throwIfMissingSecretReadValueOrDescribePermission(
environment, projectPermission,
secretPath ProjectPermissionSecretActions.DescribeSecret,
}); {
environment,
secretPath
}
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
@@ -286,10 +318,38 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( // we always check the permission against the existing environment / secret path
ProjectPermissionSecretSyncActions.Edit, // if no secret path / environment is present on the secret sync, we need to check without conditions
ProjectPermissionSub.SecretSyncs 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]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({
@@ -315,7 +375,7 @@ export const secretSyncServiceFactory = ({
if (!updatedEnvironment || !updatedSecretPath) if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" }); throw new BadRequestError({ message: "Must specify both source environment and secret path" });
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: updatedEnvironment, environment: updatedEnvironment,
secretPath: updatedSecretPath secretPath: updatedSecretPath
}); });
@@ -374,10 +434,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( if (secretSync.environment?.slug && secretSync.folder?.path) {
ProjectPermissionSecretSyncActions.Delete, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.SecretSyncs 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]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({
@@ -441,10 +511,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( if (secretSync.environment?.slug && secretSync.folder?.path) {
ProjectPermissionSecretSyncActions.SyncSecrets, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.SecretSyncs 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]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({
@@ -503,10 +583,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( if (secretSync.environment?.slug && secretSync.folder?.path) {
ProjectPermissionSecretSyncActions.ImportSecrets, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.SecretSyncs 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]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({
@@ -559,10 +649,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( if (secretSync.environment?.slug && secretSync.folder?.path) {
ProjectPermissionSecretSyncActions.RemoveSecrets, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.SecretSyncs 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]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({
@@ -12,7 +12,7 @@ Each permission consists of:
- **Subject**: The resource the permission applies to (e.g., secrets, members, settings) - **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) - **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 ## Available Project Permissions
@@ -208,6 +208,8 @@ Supports conditions and permission inversion
#### Subject: `secret-syncs` #### Subject: `secret-syncs`
Supports conditions and permission inversion.
| Action | Description | | Action | Description |
| ---------------- | -------------------------------------------------- | | ---------------- | -------------------------------------------------- |
| `read` | View secret synchronization configurations | | `read` | View secret synchronization configurations |
@@ -1,17 +1,45 @@
import { useEffect } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { subject } from "@casl/ability";
import { FilterableSelect, FormControl } from "@app/components/v2"; import { FilterableSelect, FormControl } from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput"; 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"; import { TSecretSyncForm } from "./schemas";
export const SecretSyncSourceFields = () => { export const SecretSyncSourceFields = () => {
const { control, watch } = useFormContext<TSecretSyncForm>(); const { control, watch, setError, clearErrors } = useFormContext<TSecretSyncForm>();
const { permission } = useProjectPermission();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const selectedEnvironment = watch("environment"); 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 ( return (
<> <>
@@ -263,6 +263,11 @@ export type SecretImportSubjectFields = {
secretPath: string; secretPath: string;
}; };
export type SecretSyncSubjectFields = {
environment: string;
secretPath: string;
};
export type SecretRotationSubjectFields = { export type SecretRotationSubjectFields = {
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -303,6 +308,13 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields) | (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
) )
] ]
| [
ProjectPermissionSecretSyncActions,
(
| ProjectPermissionSub.SecretSyncs
| (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
)
]
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
( (
@@ -365,7 +377,6 @@ export type ProjectPermissionSet =
] ]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@@ -291,6 +291,13 @@ export const projectRoleFormSchema = z.object({
}) })
.array() .array()
.default([]), .default([]),
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]), [ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]), [ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Groups]: GroupPolicyActionSchema.array().default([]), [ProjectPermissionSub.Groups]: GroupPolicyActionSchema.array().default([]),
@@ -342,7 +349,6 @@ export const projectRoleFormSchema = z.object({
.default([]), .default([]),
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]), [ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretSyncs]: SecretSyncPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([]), [ProjectPermissionSub.Kmip]: KmipPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretScanningDataSources]: [ProjectPermissionSub.SecretScanningDataSources]:
SecretScanningDataSourcePolicyActionSchema.array().default([]), SecretScanningDataSourcePolicyActionSchema.array().default([]),
@@ -366,7 +372,8 @@ type TConditionalFields =
| ProjectPermissionSub.CertificateTemplates | ProjectPermissionSub.CertificateTemplates
| ProjectPermissionSub.SshHosts | ProjectPermissionSub.SshHosts
| ProjectPermissionSub.SecretRotation | ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.Identity; | ProjectPermissionSub.Identity
| ProjectPermissionSub.SecretSyncs;
export const isConditionalSubjects = ( export const isConditionalSubjects = (
subject: ProjectPermissionSub subject: ProjectPermissionSub
@@ -379,7 +386,8 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.SshHosts || subject === ProjectPermissionSub.SshHosts ||
subject === ProjectPermissionSub.SecretRotation || subject === ProjectPermissionSub.SecretRotation ||
subject === ProjectPermissionSub.PkiSubscribers || subject === ProjectPermissionSub.PkiSubscribers ||
subject === ProjectPermissionSub.CertificateTemplates; subject === ProjectPermissionSub.CertificateTemplates ||
subject === ProjectPermissionSub.SecretSyncs;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema> = []; const formConditions: z.infer<typeof ConditionSchema> = [];
@@ -484,7 +492,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.SshCertificateTemplates, ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SshCertificateAuthorities, ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates, ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshHostGroups ProjectPermissionSub.SshHostGroups,
ProjectPermissionSub.SecretSyncs
].includes(subject) ].includes(subject)
) { ) {
// from above statement we are sure it won't be undefined // from above statement we are sure it won't be undefined
@@ -515,6 +524,36 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return; 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) { if (subject === ProjectPermissionSub.DynamicSecrets) {
const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential); const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential);
const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential); const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential);
@@ -777,31 +816,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
return; 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) { if (subject === ProjectPermissionSub.SecretScanningDataSources) {
const canRead = action.includes(ProjectPermissionSecretScanningDataSourceActions.Read); const canRead = action.includes(ProjectPermissionSecretScanningDataSourceActions.Read);
const canEdit = action.includes(ProjectPermissionSecretScanningDataSourceActions.Edit); const canEdit = action.includes(ProjectPermissionSecretScanningDataSourceActions.Edit);
@@ -35,6 +35,7 @@ import {
TFormSchema TFormSchema
} from "./ProjectRoleModifySection.utils"; } from "./ProjectRoleModifySection.utils";
import { SecretPermissionConditions } from "./SecretPermissionConditions"; import { SecretPermissionConditions } from "./SecretPermissionConditions";
import { SecretSyncPermissionConditions } from "./SecretSyncPermissionConditions";
import { SshHostPermissionConditions } from "./SshHostPermissionConditions"; import { SshHostPermissionConditions } from "./SshHostPermissionConditions";
type Props = { type Props = {
@@ -69,6 +70,10 @@ export const renderConditionalComponents = (
return <PkiTemplatePermissionConditions isDisabled={isDisabled} />; return <PkiTemplatePermissionConditions isDisabled={isDisabled} />;
} }
if (subject === ProjectPermissionSub.SecretSyncs) {
return <SecretSyncPermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />; 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="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>
);
};
@@ -1,4 +1,5 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { subject } from "@casl/ability";
import { import {
faBan, faBan,
faCalendarCheck, faCalendarCheck,
@@ -117,6 +118,14 @@ export const SecretSyncRow = ({
const destinationDetails = SECRET_SYNC_MAP[destination]; const destinationDetails = SECRET_SYNC_MAP[destination];
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return ( return (
<Tr <Tr
onClick={() => onClick={() =>
@@ -264,7 +273,7 @@ export const SecretSyncRow = ({
</DropdownMenuItem> </DropdownMenuItem>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.SyncSecrets} I={ProjectPermissionSecretSyncActions.SyncSecrets}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
@@ -295,7 +304,7 @@ export const SecretSyncRow = ({
{syncOption?.canImportSecrets && ( {syncOption?.canImportSecrets && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.ImportSecrets} I={ProjectPermissionSecretSyncActions.ImportSecrets}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
@@ -326,7 +335,7 @@ export const SecretSyncRow = ({
)} )}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.RemoveSecrets} I={ProjectPermissionSecretSyncActions.RemoveSecrets}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
@@ -356,7 +365,7 @@ export const SecretSyncRow = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit} I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
@@ -373,7 +382,7 @@ export const SecretSyncRow = ({
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Delete} I={ProjectPermissionSecretSyncActions.Delete}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
@@ -1,4 +1,5 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { subject } from "@casl/ability";
import { import {
faBan, faBan,
faCheck, faCheck,
@@ -63,7 +64,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
const triggerSyncSecrets = useTriggerSecretSyncSyncSecrets(); const triggerSyncSecrets = useTriggerSecretSyncSyncSecrets();
const updateSync = useUpdateSecretSync(); const updateSync = useUpdateSecretSync();
const { destination } = secretSync; const { destination, environment, folder } = secretSync;
const destinationName = SECRET_SYNC_MAP[destination].name; const destinationName = SECRET_SYNC_MAP[destination].name;
const { syncOption } = useSecretSyncOption(destination); 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 ( return (
<> <>
<div className="ml-auto mt-4 flex flex-wrap items-center justify-end gap-2"> <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> <div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.SyncSecrets} I={ProjectPermissionSecretSyncActions.SyncSecrets}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<Button <Button
@@ -194,7 +203,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
{syncOption?.canImportSecrets && ( {syncOption?.canImportSecrets && (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.ImportSecrets} I={ProjectPermissionSecretSyncActions.ImportSecrets}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
@@ -222,7 +231,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
)} )}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.RemoveSecrets} I={ProjectPermissionSecretSyncActions.RemoveSecrets}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
@@ -249,7 +258,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Edit} I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
@@ -267,7 +276,7 @@ export const SecretSyncActionTriggers = ({ secretSync }: Props) => {
</ProjectPermissionCan> </ProjectPermissionCan>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionSecretSyncActions.Delete} I={ProjectPermissionSecretSyncActions.Delete}
a={ProjectPermissionSub.SecretSyncs} a={permissionSubject}
> >
{(isAllowed: boolean) => ( {(isAllowed: boolean) => (
<DropdownMenuItem <DropdownMenuItem
@@ -1,4 +1,5 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { subject } from "@casl/ability";
import { faEdit } from "@fortawesome/free-solid-svg-icons"; import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -35,7 +36,7 @@ type Props = {
}; };
export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: 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; 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}`); throw new Error(`Unhandled Destination Section components: ${destination}`);
} }
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return ( 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 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"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Destination Configuration</h3> <h3 className="font-semibold text-mineshaft-100">Destination Configuration</h3>
<ProjectPermissionCan <ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
variant="plain" variant="plain"
@@ -1,4 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { subject } from "@casl/ability";
import { faEdit } from "@fortawesome/free-solid-svg-icons"; import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
@@ -16,7 +17,8 @@ type Props = {
}; };
export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: 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(() => { const failureMessage = useMemo(() => {
if (syncStatus === SecretSyncStatus.Failed) { if (syncStatus === SecretSyncStatus.Failed) {
@@ -32,14 +34,19 @@ export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) =
return null; return null;
}, [syncStatus, lastSyncMessage]); }, [syncStatus, lastSyncMessage]);
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return ( 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 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"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Details</h3> <h3 className="font-semibold text-mineshaft-100">Details</h3>
<ProjectPermissionCan <ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
variant="plain" variant="plain"
@@ -1,4 +1,5 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { subject } from "@casl/ability";
import { faEdit } from "@fortawesome/free-solid-svg-icons"; import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -21,7 +22,9 @@ type Props = {
export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => { export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => {
const { const {
destination, destination,
syncOptions: { initialSyncBehavior, disableSecretDeletion, keySchema } syncOptions: { initialSyncBehavior, disableSecretDeletion, keySchema },
environment,
folder
} = secretSync; } = secretSync;
let AdditionalSyncOptionsComponent: ReactNode; let AdditionalSyncOptionsComponent: ReactNode;
@@ -59,15 +62,20 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) =
throw new Error(`Unhandled Destination Review Fields: ${destination}`); throw new Error(`Unhandled Destination Review Fields: ${destination}`);
} }
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return ( return (
<div> <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 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"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
<h3 className="font-semibold text-mineshaft-100">Sync Options</h3> <h3 className="font-semibold text-mineshaft-100">Sync Options</h3>
<ProjectPermissionCan <ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
variant="plain" variant="plain"
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faEdit, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import { faEdit, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -16,6 +17,14 @@ type Props = {
export const SecretSyncSourceSection = ({ secretSync, onEditSource }: Props) => { export const SecretSyncSourceSection = ({ secretSync, onEditSource }: Props) => {
const { folder, environment } = secretSync; const { folder, environment } = secretSync;
const permissionSubject =
environment && folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: environment.slug,
secretPath: folder.path
})
: ProjectPermissionSub.SecretSyncs;
return ( return (
<div> <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 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> </div>
</Tooltip> </Tooltip>
)} )}
<ProjectPermissionCan <ProjectPermissionCan I={ProjectPermissionSecretSyncActions.Edit} a={permissionSubject}>
I={ProjectPermissionSecretSyncActions.Edit}
a={ProjectPermissionSub.SecretSyncs}
>
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
variant="plain" variant="plain"