feat(rbac): fixed broken view when clicking editing predefined role

This commit is contained in:
Akhil Mohan
2023-09-05 12:25:06 +05:30
parent 6bbdc4a405
commit 26028e7312
7 changed files with 202 additions and 61 deletions

View File

@@ -1,14 +1,4 @@
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 {
CreateRoleSchema,
DeleteRoleSchema,
@@ -17,12 +7,23 @@ import {
GetUserProjectPermission,
UpdateRoleSchema
} from "../../validation";
import { packRules } from "@casl/ability/extra";
import {
adminProjectPermissions,
getUserProjectPermissions,
memberProjectPermissions,
viewerProjectPermission
} 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) => {
const {
@@ -137,7 +138,9 @@ export const getRoles = async (req: Request, res: Response) => {
}
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 = [
// owner is only in org level role
...(isOrgRole
? [
{
@@ -161,8 +164,9 @@ export const getRoles = async (req: Request, res: Response) => {
name: "Member",
slug: "member",
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
? []
: [
@@ -171,7 +175,7 @@ export const getRoles = async (req: Request, res: Response) => {
name: "Viewer",
slug: "viewer",
description: "Non-administrative role in an organization",
permissions: isOrgRole ? viewerProjectPermission.rules : viewerProjectPermission.rules
permissions: viewerProjectPermission.rules
}
]),
...customRoles
@@ -203,6 +207,7 @@ export const getUserWorkspacePermissions = async (req: Request, res: Response) =
params: { workspaceId }
} = await validateRequest(GetUserProjectPermission, req);
const { permission } = await getUserProjectPermissions(req.user.id, workspaceId);
res.status(200).json({
data: {
permissions: packRules(permission.rules)

View File

@@ -142,7 +142,7 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace);
return build();
};

View File

@@ -38,23 +38,15 @@ export type TFormSchema = z.infer<typeof formSchema>;
// convert role permission to form compatiable data structure
export const rolePermission2Form = (permissions: TPermission[] = []) => {
const formVal: TFormSchema["permissions"] = {
workspace: {},
billing: {},
settings: {},
role: {},
sso: {},
member: {},
"service-account": {},
"incident-contact": {},
"secret-scanning": {}
};
const formVal: Partial<TFormSchema["permissions"]> = {};
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
const key = permission.subject as keyof TFormSchema["permissions"];
(formVal[key] as Exclude<TFormSchema["permissions"]["member"], undefined>)[permission.action] =
true;
const key = subject as keyof TFormSchema["permissions"];
(formVal[key] as Exclude<TFormSchema["permissions"]["member"], undefined>)[action] = true;
});
return formVal;

View File

@@ -33,6 +33,7 @@ import {
rolePermission2Form,
TFormSchema
} from "./ProjectRoleModifySection.utils";
import { SecretRollbackPermission } from "./SecretRollbackPermission";
import { SingleProjectPermission } from "./SingleProjectPermission";
import { WsProjectPermission } from "./WsProjectPermission";
@@ -226,7 +227,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
/>
</div>
</div>
<div className="flex flex-col space-y-4">
<div>
<MultiEnvProjectPermission
isNonEditable={isNonEditable}
control={control}
@@ -237,7 +238,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
formName="secrets"
/>
</div>
<div className="flex flex-col space-y-4">
<div>
<MultiEnvProjectPermission
isNonEditable={isNonEditable}
control={control}
@@ -248,7 +249,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
formName="folders"
/>
</div>
<div className="flex flex-col space-y-4">
<div>
<MultiEnvProjectPermission
isNonEditable={isNonEditable}
control={control}
@@ -259,6 +260,13 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
formName="secret-imports"
/>
</div>
<div key="permission-ws">
<WsProjectPermission
control={control}
setValue={setValue}
isNonEditable={isNonEditable}
/>
</div>
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
<div key={`permission-${title}`}>
<SingleProjectPermission
@@ -272,8 +280,8 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
/>
</div>
))}
<div className="flex flex-col space-y-4" key="permission-ws">
<WsProjectPermission
<div key="permission-secret-rollback">
<SecretRollbackPermission
control={control}
setValue={setValue}
isNonEditable={isNonEditable}

View File

@@ -48,6 +48,12 @@ export const formSchema = z.object({
edit: z.boolean().optional(),
delete: z.boolean().optional()
})
.optional(),
"secret-rollback": z
.object({
read: z.boolean().optional(),
create: z.boolean().optional()
})
.optional()
})
});
@@ -75,37 +81,23 @@ const multiEnvApi2Form = (
// convert role permission to form compatiable data structure
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const formVal: TFormSchema["permissions"] = {
secrets: {},
folders: {},
integrations: {},
settings: {},
role: {},
member: {},
"service-tokens": {},
workspace: {},
environments: {},
tags: {},
webhooks: {},
"audit-logs": {},
"ip-allowlist": {},
"secret-imports": {}
};
const formVal: Partial<TFormSchema["permissions"]> = {};
permissions.forEach((permission) => {
if (["secrets", "folders", "secret-imports"].includes(permission.subject)) {
multiEnvApi2Form(
formVal[permission.subject] as TFormSchema["permissions"]["secrets"],
permission
);
const { subject, action } = permission;
if (!formVal?.[subject]) formVal[subject] = {};
if (["secrets", "folders", "secret-imports"].includes(subject)) {
multiEnvApi2Form(formVal[subject] as TFormSchema["permissions"]["secrets"], permission);
} else {
// everything else follows same pattern
// formVal[settings][read | write] = true
const key = permission.subject as keyof Omit<
TFormSchema["permissions"],
"secrets" | "workspace"
>;
formVal[key]![permission.action] = true;
formVal[
subject as keyof Omit<
TFormSchema["permissions"],
"secrets" | "workspace" | "secret-rollback"
>
]![action] = true;
}
});

View File

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

View File

@@ -18,7 +18,6 @@ type Props = {
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
@@ -92,7 +91,6 @@ export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props)
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>