mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: added secret folder rbac'
This commit is contained in:
@@ -23,6 +23,7 @@ export enum ProjectPermissionSub {
|
||||
IpAllowList = "ip-allowlist",
|
||||
Project = "workspace",
|
||||
Secrets = "secrets",
|
||||
SecretFolders = "secret-folders",
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation",
|
||||
@@ -42,6 +43,10 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
|
||||
6
backend/src/services/secret-folder/secret-folder-fns.ts
Normal file
6
backend/src/services/secret-folder/secret-folder-fns.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { RawRule } from "@casl/ability";
|
||||
|
||||
import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
|
||||
export const shouldCheckFolderPermission = (rules: RawRule[]) =>
|
||||
rules.some((rule) => (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders));
|
||||
@@ -11,6 +11,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||
import { shouldCheckFolderPermission } from "./secret-folder-fns";
|
||||
import {
|
||||
TCreateFolderDTO,
|
||||
TDeleteFolderDTO,
|
||||
@@ -57,10 +58,21 @@ export const secretFolderServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
// we do this because we've split Secret and SecretFolder resources
|
||||
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||
// for backwards compatibility, we handle authorization only when SecretFolders subject is used
|
||||
if (shouldCheckFolderPermission(permission.rules)) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
|
||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||
@@ -148,10 +160,20 @@ export const secretFolderServiceFactory = ({
|
||||
);
|
||||
|
||||
folders.forEach(({ environment, path: secretPath }) => {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
// we do this because we've split Secret and SecretFolder resources
|
||||
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||
// for backwards compatibility, we handle authorization only when SecretFolders subject is used
|
||||
if (shouldCheckFolderPermission(permission.rules)) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const result = await folderDAL.transaction(async (tx) =>
|
||||
@@ -243,10 +265,21 @@ export const secretFolderServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
// we do this because we've split Secret and SecretFolder resources
|
||||
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||
// for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
|
||||
if (shouldCheckFolderPermission(permission.rules)) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
|
||||
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
||||
@@ -316,10 +349,21 @@ export const secretFolderServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
// we do this because we've split Secret and SecretFolder resources
|
||||
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||
// for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
|
||||
if (shouldCheckFolderPermission(permission.rules)) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||
);
|
||||
} else {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
|
||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||
|
||||
@@ -21,6 +21,7 @@ export enum ProjectPermissionSub {
|
||||
IpAllowList = "ip-allowlist",
|
||||
Workspace = "workspace",
|
||||
Secrets = "secrets",
|
||||
SecretFolders = "secret-folders",
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation",
|
||||
|
||||
@@ -36,6 +36,7 @@ export const formSchema = z.object({
|
||||
permissions: z
|
||||
.object({
|
||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
||||
"secret-folders": generalPermissionSchema.optional(),
|
||||
member: generalPermissionSchema,
|
||||
groups: generalPermissionSchema,
|
||||
identity: generalPermissionSchema,
|
||||
@@ -158,7 +159,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
||||
if (rule === "secrets") {
|
||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule);
|
||||
} else {
|
||||
} else if (actions) {
|
||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||
if (isAllowed) {
|
||||
permissions.push({ subject: rule, action });
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Control, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
|
||||
import { Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { ProjectPermissionSub } from "@app/context";
|
||||
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
SameAsSecrets = "same-as-secrets",
|
||||
ReadOnly = "read-only"
|
||||
}
|
||||
|
||||
export const RowPermissionSecretFoldersRow = ({ isEditable, setValue, control }: Props) => {
|
||||
const formName = ProjectPermissionSub.SecretFolders;
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: `permissions.${formName}`
|
||||
});
|
||||
|
||||
const selectedPermissionCategory =
|
||||
rule !== undefined ? Permission.ReadOnly : Permission.SameAsSecrets;
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (!val) return;
|
||||
switch (val) {
|
||||
case Permission.SameAsSecrets: {
|
||||
setValue(`permissions.${formName}`, undefined, { shouldDirty: true });
|
||||
break;
|
||||
}
|
||||
// Read-only
|
||||
default:
|
||||
setValue(
|
||||
`permissions.${formName}`,
|
||||
{
|
||||
read: true,
|
||||
edit: false,
|
||||
create: false,
|
||||
delete: false
|
||||
},
|
||||
{
|
||||
shouldDirty: true
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td />
|
||||
<Td>Secret Folders</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={selectedPermissionCategory}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={handlePermissionChange}
|
||||
isDisabled={!isEditable}
|
||||
>
|
||||
<SelectItem value={Permission.SameAsSecrets}>Same as Secrets</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
</Select>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
||||
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
import { RowPermissionSecretFoldersRow } from "./RolePermissionSecretFoldersRow";
|
||||
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
|
||||
|
||||
const SINGLE_PERMISSION_LIST = [
|
||||
@@ -177,6 +178,11 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => {
|
||||
getValue={getValues}
|
||||
control={control}
|
||||
/>
|
||||
<RowPermissionSecretFoldersRow
|
||||
isEditable={isCustomRole}
|
||||
setValue={setValue}
|
||||
control={control}
|
||||
/>
|
||||
{SINGLE_PERMISSION_LIST.map((permission) => {
|
||||
return (
|
||||
<RolePermissionRow
|
||||
|
||||
@@ -44,7 +44,12 @@ import {
|
||||
Tooltip,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
|
||||
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
|
||||
@@ -121,6 +126,12 @@ export const ActionBar = ({
|
||||
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
||||
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
||||
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const shouldCheckFolderPermission = permission.rules.some((rule) =>
|
||||
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
|
||||
);
|
||||
|
||||
const debouncedOnSearch = debounce(onSearchChange, 500);
|
||||
|
||||
const handleFolderCreate = async (folderName: string) => {
|
||||
@@ -411,7 +422,12 @@ export const ActionBar = ({
|
||||
<div className="flex flex-col space-y-1 p-1.5">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
a={subject(
|
||||
shouldCheckFolderPermission
|
||||
? ProjectPermissionSub.SecretFolders
|
||||
: ProjectPermissionSub.Secrets,
|
||||
{ environment, secretPath }
|
||||
)}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, IconButton, Modal, ModalContent } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||
@@ -36,6 +36,11 @@ export const FolderListView = ({
|
||||
"deleteFolder"
|
||||
] as const);
|
||||
const router = useRouter();
|
||||
const { permission } = useProjectPermission();
|
||||
|
||||
const shouldCheckFolderPermission = permission.rules.some((rule) =>
|
||||
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
|
||||
);
|
||||
|
||||
const { mutateAsync: updateFolder } = useUpdateFolder();
|
||||
const { mutateAsync: deleteFolder } = useDeleteFolder();
|
||||
@@ -128,7 +133,12 @@ export const FolderListView = ({
|
||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
a={subject(
|
||||
shouldCheckFolderPermission
|
||||
? ProjectPermissionSub.SecretFolders
|
||||
: ProjectPermissionSub.Secrets,
|
||||
{ environment, secretPath }
|
||||
)}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
@@ -147,7 +157,12 @@ export const FolderListView = ({
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
a={subject(
|
||||
shouldCheckFolderPermission
|
||||
? ProjectPermissionSub.SecretFolders
|
||||
: ProjectPermissionSub.Secrets,
|
||||
{ environment, secretPath }
|
||||
)}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user