mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 19:28:33 +00:00
feat(rbac): fixed broken view when clicking editing predefined role
This commit is contained in:
@@ -1,14 +1,4 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { BadRequestError } from "../../utils/errors";
|
|
||||||
import Role from "../../models/role";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
adminPermissions,
|
|
||||||
getUserOrgPermissions,
|
|
||||||
memberPermissions
|
|
||||||
} from "../../services/RoleService";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import {
|
import {
|
||||||
CreateRoleSchema,
|
CreateRoleSchema,
|
||||||
DeleteRoleSchema,
|
DeleteRoleSchema,
|
||||||
@@ -17,12 +7,23 @@ import {
|
|||||||
GetUserProjectPermission,
|
GetUserProjectPermission,
|
||||||
UpdateRoleSchema
|
UpdateRoleSchema
|
||||||
} from "../../validation";
|
} from "../../validation";
|
||||||
import { packRules } from "@casl/ability/extra";
|
|
||||||
import {
|
import {
|
||||||
adminProjectPermissions,
|
adminProjectPermissions,
|
||||||
getUserProjectPermissions,
|
getUserProjectPermissions,
|
||||||
|
memberProjectPermissions,
|
||||||
viewerProjectPermission
|
viewerProjectPermission
|
||||||
} from "../../services/ProjectRoleService";
|
} from "../../services/ProjectRoleService";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
adminPermissions,
|
||||||
|
getUserOrgPermissions,
|
||||||
|
memberPermissions
|
||||||
|
} from "../../services/RoleService";
|
||||||
|
import { BadRequestError } from "../../utils/errors";
|
||||||
|
import Role from "../../models/role";
|
||||||
|
import { validateRequest } from "../../helpers/validation";
|
||||||
|
import { packRules } from "@casl/ability/extra";
|
||||||
|
|
||||||
export const createRole = async (req: Request, res: Response) => {
|
export const createRole = async (req: Request, res: Response) => {
|
||||||
const {
|
const {
|
||||||
@@ -137,7 +138,9 @@ export const getRoles = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
|
const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
|
||||||
|
// as this is shared between org and workspace switch the rule set based on it
|
||||||
const roles = [
|
const roles = [
|
||||||
|
// owner is only in org level role
|
||||||
...(isOrgRole
|
...(isOrgRole
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -161,8 +164,9 @@ export const getRoles = async (req: Request, res: Response) => {
|
|||||||
name: "Member",
|
name: "Member",
|
||||||
slug: "member",
|
slug: "member",
|
||||||
description: "Non-administrative role in an organization",
|
description: "Non-administrative role in an organization",
|
||||||
permissions: isOrgRole ? memberPermissions.rules : adminProjectPermissions.rules
|
permissions: isOrgRole ? memberPermissions.rules : memberProjectPermissions.rules
|
||||||
},
|
},
|
||||||
|
// viewer role only for project level
|
||||||
...(isOrgRole
|
...(isOrgRole
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
@@ -171,7 +175,7 @@ export const getRoles = async (req: Request, res: Response) => {
|
|||||||
name: "Viewer",
|
name: "Viewer",
|
||||||
slug: "viewer",
|
slug: "viewer",
|
||||||
description: "Non-administrative role in an organization",
|
description: "Non-administrative role in an organization",
|
||||||
permissions: isOrgRole ? viewerProjectPermission.rules : viewerProjectPermission.rules
|
permissions: viewerProjectPermission.rules
|
||||||
}
|
}
|
||||||
]),
|
]),
|
||||||
...customRoles
|
...customRoles
|
||||||
@@ -203,6 +207,7 @@ export const getUserWorkspacePermissions = async (req: Request, res: Response) =
|
|||||||
params: { workspaceId }
|
params: { workspaceId }
|
||||||
} = await validateRequest(GetUserProjectPermission, req);
|
} = await validateRequest(GetUserProjectPermission, req);
|
||||||
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
|
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
data: {
|
data: {
|
||||||
permissions: packRules(permission.rules)
|
permissions: packRules(permission.rules)
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const buildAdminPermission = () => {
|
|||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace);
|
||||||
|
|
||||||
return build();
|
return build();
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-14
@@ -38,23 +38,15 @@ export type TFormSchema = z.infer<typeof formSchema>;
|
|||||||
|
|
||||||
// convert role permission to form compatiable data structure
|
// convert role permission to form compatiable data structure
|
||||||
export const rolePermission2Form = (permissions: TPermission[] = []) => {
|
export const rolePermission2Form = (permissions: TPermission[] = []) => {
|
||||||
const formVal: TFormSchema["permissions"] = {
|
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||||
workspace: {},
|
|
||||||
billing: {},
|
|
||||||
settings: {},
|
|
||||||
role: {},
|
|
||||||
sso: {},
|
|
||||||
member: {},
|
|
||||||
"service-account": {},
|
|
||||||
"incident-contact": {},
|
|
||||||
"secret-scanning": {}
|
|
||||||
};
|
|
||||||
|
|
||||||
permissions.forEach((permission) => {
|
permissions.forEach((permission) => {
|
||||||
|
const { subject, action } = permission;
|
||||||
|
if (!formVal?.[subject]) formVal[subject] = {};
|
||||||
|
|
||||||
// akhilmhdh: this is typecast as something other than workspace key else i would need an if loop with same condition on both side
|
// akhilmhdh: this is typecast as something other than workspace key else i would need an if loop with same condition on both side
|
||||||
const key = permission.subject as keyof TFormSchema["permissions"];
|
const key = subject as keyof TFormSchema["permissions"];
|
||||||
(formVal[key] as Exclude<TFormSchema["permissions"]["member"], undefined>)[permission.action] =
|
(formVal[key] as Exclude<TFormSchema["permissions"]["member"], undefined>)[action] = true;
|
||||||
true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return formVal;
|
return formVal;
|
||||||
|
|||||||
+13
-5
@@ -33,6 +33,7 @@ import {
|
|||||||
rolePermission2Form,
|
rolePermission2Form,
|
||||||
TFormSchema
|
TFormSchema
|
||||||
} from "./ProjectRoleModifySection.utils";
|
} from "./ProjectRoleModifySection.utils";
|
||||||
|
import { SecretRollbackPermission } from "./SecretRollbackPermission";
|
||||||
import { SingleProjectPermission } from "./SingleProjectPermission";
|
import { SingleProjectPermission } from "./SingleProjectPermission";
|
||||||
import { WsProjectPermission } from "./WsProjectPermission";
|
import { WsProjectPermission } from "./WsProjectPermission";
|
||||||
|
|
||||||
@@ -226,7 +227,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<div>
|
||||||
<MultiEnvProjectPermission
|
<MultiEnvProjectPermission
|
||||||
isNonEditable={isNonEditable}
|
isNonEditable={isNonEditable}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -237,7 +238,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
formName="secrets"
|
formName="secrets"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<div>
|
||||||
<MultiEnvProjectPermission
|
<MultiEnvProjectPermission
|
||||||
isNonEditable={isNonEditable}
|
isNonEditable={isNonEditable}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -248,7 +249,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
formName="folders"
|
formName="folders"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<div>
|
||||||
<MultiEnvProjectPermission
|
<MultiEnvProjectPermission
|
||||||
isNonEditable={isNonEditable}
|
isNonEditable={isNonEditable}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -259,6 +260,13 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
formName="secret-imports"
|
formName="secret-imports"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div key="permission-ws">
|
||||||
|
<WsProjectPermission
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
isNonEditable={isNonEditable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
|
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
|
||||||
<div key={`permission-${title}`}>
|
<div key={`permission-${title}`}>
|
||||||
<SingleProjectPermission
|
<SingleProjectPermission
|
||||||
@@ -272,8 +280,8 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="flex flex-col space-y-4" key="permission-ws">
|
<div key="permission-secret-rollback">
|
||||||
<WsProjectPermission
|
<SecretRollbackPermission
|
||||||
control={control}
|
control={control}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
isNonEditable={isNonEditable}
|
isNonEditable={isNonEditable}
|
||||||
|
|||||||
+18
-26
@@ -48,6 +48,12 @@ export const formSchema = z.object({
|
|||||||
edit: z.boolean().optional(),
|
edit: z.boolean().optional(),
|
||||||
delete: z.boolean().optional()
|
delete: z.boolean().optional()
|
||||||
})
|
})
|
||||||
|
.optional(),
|
||||||
|
"secret-rollback": z
|
||||||
|
.object({
|
||||||
|
read: z.boolean().optional(),
|
||||||
|
create: z.boolean().optional()
|
||||||
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -75,37 +81,23 @@ const multiEnvApi2Form = (
|
|||||||
|
|
||||||
// convert role permission to form compatiable data structure
|
// convert role permission to form compatiable data structure
|
||||||
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||||
const formVal: TFormSchema["permissions"] = {
|
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||||
secrets: {},
|
|
||||||
folders: {},
|
|
||||||
integrations: {},
|
|
||||||
settings: {},
|
|
||||||
role: {},
|
|
||||||
member: {},
|
|
||||||
"service-tokens": {},
|
|
||||||
workspace: {},
|
|
||||||
environments: {},
|
|
||||||
tags: {},
|
|
||||||
webhooks: {},
|
|
||||||
"audit-logs": {},
|
|
||||||
"ip-allowlist": {},
|
|
||||||
"secret-imports": {}
|
|
||||||
};
|
|
||||||
|
|
||||||
permissions.forEach((permission) => {
|
permissions.forEach((permission) => {
|
||||||
if (["secrets", "folders", "secret-imports"].includes(permission.subject)) {
|
const { subject, action } = permission;
|
||||||
multiEnvApi2Form(
|
if (!formVal?.[subject]) formVal[subject] = {};
|
||||||
formVal[permission.subject] as TFormSchema["permissions"]["secrets"],
|
|
||||||
permission
|
if (["secrets", "folders", "secret-imports"].includes(subject)) {
|
||||||
);
|
multiEnvApi2Form(formVal[subject] as TFormSchema["permissions"]["secrets"], permission);
|
||||||
} else {
|
} else {
|
||||||
// everything else follows same pattern
|
// everything else follows same pattern
|
||||||
// formVal[settings][read | write] = true
|
// formVal[settings][read | write] = true
|
||||||
const key = permission.subject as keyof Omit<
|
formVal[
|
||||||
TFormSchema["permissions"],
|
subject as keyof Omit<
|
||||||
"secrets" | "workspace"
|
TFormSchema["permissions"],
|
||||||
>;
|
"secrets" | "workspace" | "secret-rollback"
|
||||||
formVal[key]![permission.action] = true;
|
>
|
||||||
|
]![action] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||||
|
import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Checkbox, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isNonEditable?: boolean;
|
||||||
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
control: Control<TFormSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Permission {
|
||||||
|
NoAccess = "no-access",
|
||||||
|
ReadOnly = "read-only",
|
||||||
|
FullAccess = "full-acess",
|
||||||
|
Custom = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERMISSIONS = [
|
||||||
|
{ action: "create", label: "Perform Rollback" },
|
||||||
|
{ action: "read", label: "View" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const SecretRollbackPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||||
|
const rule = useWatch({
|
||||||
|
control,
|
||||||
|
name: "permissions.secret-rollback"
|
||||||
|
});
|
||||||
|
const [isCustom, setIsCustom] = useToggle();
|
||||||
|
|
||||||
|
const selectedPermissionCategory = useMemo(() => {
|
||||||
|
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||||
|
const totalActions = PERMISSIONS.length;
|
||||||
|
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||||
|
|
||||||
|
if (isCustom) return Permission.Custom;
|
||||||
|
if (score === 0) return Permission.NoAccess;
|
||||||
|
if (score === totalActions) return Permission.FullAccess;
|
||||||
|
|
||||||
|
return Permission.Custom;
|
||||||
|
}, [rule, isCustom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||||
|
else setIsCustom.off();
|
||||||
|
}, [selectedPermissionCategory]);
|
||||||
|
|
||||||
|
const handlePermissionChange = (val: Permission) => {
|
||||||
|
if (val === Permission.Custom) setIsCustom.on();
|
||||||
|
else setIsCustom.off();
|
||||||
|
|
||||||
|
switch (val) {
|
||||||
|
case Permission.NoAccess:
|
||||||
|
setValue(
|
||||||
|
"permissions.secret-rollback",
|
||||||
|
{ read: false, create: false },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Permission.FullAccess:
|
||||||
|
setValue(
|
||||||
|
"permissions.secret-rollback",
|
||||||
|
{ read: true, create: true },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Permission.ReadOnly:
|
||||||
|
setValue(
|
||||||
|
"permissions.secret-rollback",
|
||||||
|
{ read: true, create: false },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setValue(
|
||||||
|
"permissions.secret-rollback",
|
||||||
|
{ read: false, create: false },
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"px-10 py-6 bg-mineshaft-800 rounded-md",
|
||||||
|
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow flex flex-col">
|
||||||
|
<div className="font-medium mb-1 text-lg">Secret Rollback</div>
|
||||||
|
<div className="text-xs font-light">Secret rollback control actions</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
defaultValue={Permission.NoAccess}
|
||||||
|
isDisabled={isNonEditable}
|
||||||
|
value={selectedPermissionCategory}
|
||||||
|
onValueChange={handlePermissionChange}
|
||||||
|
>
|
||||||
|
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||||
|
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
initial={false}
|
||||||
|
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
|
||||||
|
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
|
||||||
|
>
|
||||||
|
{isCustom &&
|
||||||
|
PERMISSIONS.map(({ action, label }) => (
|
||||||
|
<Controller
|
||||||
|
name={`permissions.secret-rollback.${action}`}
|
||||||
|
key={`permissions.secret-rollback.${action}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
id={`permissions.secret-rollback.${action}`}
|
||||||
|
isDisabled={isNonEditable}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
-2
@@ -18,7 +18,6 @@ type Props = {
|
|||||||
|
|
||||||
enum Permission {
|
enum Permission {
|
||||||
NoAccess = "no-access",
|
NoAccess = "no-access",
|
||||||
ReadOnly = "read-only",
|
|
||||||
FullAccess = "full-acess",
|
FullAccess = "full-acess",
|
||||||
Custom = "custom"
|
Custom = "custom"
|
||||||
}
|
}
|
||||||
@@ -92,7 +91,6 @@ export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props)
|
|||||||
onValueChange={handlePermissionChange}
|
onValueChange={handlePermissionChange}
|
||||||
>
|
>
|
||||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
|
||||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
Reference in New Issue
Block a user