mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 02:26:40 +00:00
Merge pull request #2220 from Infisical/feat/added-secret-folder-rbac
feat: added secret folder permissions
This commit is contained in:
@@ -23,6 +23,7 @@ export enum ProjectPermissionSub {
|
|||||||
IpAllowList = "ip-allowlist",
|
IpAllowList = "ip-allowlist",
|
||||||
Project = "workspace",
|
Project = "workspace",
|
||||||
Secrets = "secrets",
|
Secrets = "secrets",
|
||||||
|
SecretFolders = "secret-folders",
|
||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
@@ -42,6 +43,10 @@ export type ProjectPermissionSet =
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||||
]
|
]
|
||||||
|
| [
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
||||||
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||||
|
|||||||
@@ -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 { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||||
|
import { shouldCheckFolderPermission } from "./secret-folder-fns";
|
||||||
import {
|
import {
|
||||||
TCreateFolderDTO,
|
TCreateFolderDTO,
|
||||||
TDeleteFolderDTO,
|
TDeleteFolderDTO,
|
||||||
@@ -57,10 +58,21 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
// 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 });
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
@@ -148,10 +160,20 @@ export const secretFolderServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
folders.forEach(({ environment, path: secretPath }) => {
|
folders.forEach(({ environment, path: secretPath }) => {
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
ProjectPermissionActions.Edit,
|
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
// 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) =>
|
const result = await folderDAL.transaction(async (tx) =>
|
||||||
@@ -243,10 +265,21 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
// 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);
|
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
||||||
@@ -316,10 +349,21 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
// 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 });
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export enum ProjectPermissionSub {
|
|||||||
IpAllowList = "ip-allowlist",
|
IpAllowList = "ip-allowlist",
|
||||||
Workspace = "workspace",
|
Workspace = "workspace",
|
||||||
Secrets = "secrets",
|
Secrets = "secrets",
|
||||||
|
SecretFolders = "secret-folders",
|
||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
|
|||||||
+2
-1
@@ -36,6 +36,7 @@ export const formSchema = z.object({
|
|||||||
permissions: z
|
permissions: z
|
||||||
.object({
|
.object({
|
||||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
secrets: z.record(multiEnvPermissionSchema).optional(),
|
||||||
|
"secret-folders": generalPermissionSchema.optional(),
|
||||||
member: generalPermissionSchema,
|
member: generalPermissionSchema,
|
||||||
groups: generalPermissionSchema,
|
groups: generalPermissionSchema,
|
||||||
identity: generalPermissionSchema,
|
identity: generalPermissionSchema,
|
||||||
@@ -158,7 +159,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
|||||||
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
||||||
if (rule === "secrets") {
|
if (rule === "secrets") {
|
||||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule);
|
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule);
|
||||||
} else {
|
} else if (actions) {
|
||||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||||
if (isAllowed) {
|
if (isAllowed) {
|
||||||
permissions.push({ subject: rule, action });
|
permissions.push({ subject: rule, action });
|
||||||
|
|||||||
+71
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+6
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
import { RolePermissionRow } from "./RolePermissionRow";
|
import { RolePermissionRow } from "./RolePermissionRow";
|
||||||
|
import { RowPermissionSecretFoldersRow } from "./RolePermissionSecretFoldersRow";
|
||||||
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
|
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
|
||||||
|
|
||||||
const SINGLE_PERMISSION_LIST = [
|
const SINGLE_PERMISSION_LIST = [
|
||||||
@@ -177,6 +178,11 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => {
|
|||||||
getValue={getValues}
|
getValue={getValues}
|
||||||
control={control}
|
control={control}
|
||||||
/>
|
/>
|
||||||
|
<RowPermissionSecretFoldersRow
|
||||||
|
isEditable={isCustomRole}
|
||||||
|
setValue={setValue}
|
||||||
|
control={control}
|
||||||
|
/>
|
||||||
{SINGLE_PERMISSION_LIST.map((permission) => {
|
{SINGLE_PERMISSION_LIST.map((permission) => {
|
||||||
return (
|
return (
|
||||||
<RolePermissionRow
|
<RolePermissionRow
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} 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 { usePopUp } from "@app/hooks";
|
||||||
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
|
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
|
||||||
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
|
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
|
||||||
@@ -121,6 +126,12 @@ export const ActionBar = ({
|
|||||||
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
||||||
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
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 debouncedOnSearch = debounce(onSearchChange, 500);
|
||||||
|
|
||||||
const handleFolderCreate = async (folderName: string) => {
|
const handleFolderCreate = async (folderName: string) => {
|
||||||
@@ -411,7 +422,12 @@ export const ActionBar = ({
|
|||||||
<div className="flex flex-col space-y-1 p-1.5">
|
<div className="flex flex-col space-y-1 p-1.5">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(
|
||||||
|
shouldCheckFolderPermission
|
||||||
|
? ProjectPermissionSub.SecretFolders
|
||||||
|
: ProjectPermissionSub.Secrets,
|
||||||
|
{ environment, secretPath }
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { DeleteActionModal, IconButton, Modal, ModalContent } from "@app/components/v2";
|
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 { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
||||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||||
@@ -36,6 +36,11 @@ export const FolderListView = ({
|
|||||||
"deleteFolder"
|
"deleteFolder"
|
||||||
] as const);
|
] as const);
|
||||||
const router = useRouter();
|
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: updateFolder } = useUpdateFolder();
|
||||||
const { mutateAsync: deleteFolder } = useDeleteFolder();
|
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">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(
|
||||||
|
shouldCheckFolderPermission
|
||||||
|
? ProjectPermissionSub.SecretFolders
|
||||||
|
: ProjectPermissionSub.Secrets,
|
||||||
|
{ environment, secretPath }
|
||||||
|
)}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Edit"
|
allowedLabel="Edit"
|
||||||
>
|
>
|
||||||
@@ -147,7 +157,12 @@ export const FolderListView = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(
|
||||||
|
shouldCheckFolderPermission
|
||||||
|
? ProjectPermissionSub.SecretFolders
|
||||||
|
: ProjectPermissionSub.Secrets,
|
||||||
|
{ environment, secretPath }
|
||||||
|
)}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user