mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 07:29:35 +00:00
feat(rbac): made changes from testing with maidul
This commit is contained in:
@@ -191,7 +191,15 @@ export const createWorkspace = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.Workspace
|
||||
);
|
||||
|
||||
// delete workspace
|
||||
await deleteWork({
|
||||
@@ -246,7 +254,14 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
const integrations = await Integration.find({
|
||||
workspace: workspaceId
|
||||
|
||||
@@ -44,6 +44,14 @@ router.post(
|
||||
workspaceController.createWorkspace
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:workspaceId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
workspaceController.deleteWorkspace
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:workspaceId/name",
|
||||
requireAuth({
|
||||
|
||||
@@ -7,12 +7,18 @@ import { Tooltip } from "../v2";
|
||||
|
||||
type Props = {
|
||||
label?: ReactNode;
|
||||
// this prop is used when there exist already a tooltip as helper text for users
|
||||
// so when permission is allowed same tooltip will be reused to show helpertext
|
||||
renderTooltip?: boolean;
|
||||
allowedLabel?: string;
|
||||
} & BoundCanProps<TOrgPermission>;
|
||||
|
||||
export const OrgPermissionCan: FunctionComponent<Props> = ({
|
||||
label = "Permission Denied. Kindly contact your org admin",
|
||||
children,
|
||||
passThrough = true,
|
||||
renderTooltip,
|
||||
allowedLabel,
|
||||
...props
|
||||
}) => {
|
||||
const permission = useOrgPermission();
|
||||
@@ -30,6 +36,10 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
|
||||
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
||||
}
|
||||
|
||||
if (isAllowed && renderTooltip) {
|
||||
return <Tooltip content={allowedLabel}>{finalChild}</Tooltip>;
|
||||
}
|
||||
|
||||
if (!isAllowed) return null;
|
||||
|
||||
return finalChild;
|
||||
|
||||
@@ -7,12 +7,18 @@ import { Tooltip } from "../v2";
|
||||
|
||||
type Props = {
|
||||
label?: ReactNode;
|
||||
// this prop is used when there exist already a tooltip as helper text for users
|
||||
// so when permission is allowed same tooltip will be reused to show helpertext
|
||||
renderTooltip?: boolean;
|
||||
allowedLabel?: string;
|
||||
} & BoundCanProps<TProjectPermission>;
|
||||
|
||||
export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
||||
label = "Permission Denied. Kindly contact your project admin",
|
||||
children,
|
||||
passThrough = true,
|
||||
renderTooltip,
|
||||
allowedLabel,
|
||||
...props
|
||||
}) => {
|
||||
const permission = useProjectPermission();
|
||||
@@ -30,6 +36,10 @@ export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
||||
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
||||
}
|
||||
|
||||
if (isAllowed && renderTooltip) {
|
||||
return <Tooltip content={allowedLabel}>{finalChild}</Tooltip>;
|
||||
}
|
||||
|
||||
if (!isAllowed) return null;
|
||||
|
||||
return finalChild;
|
||||
|
||||
@@ -45,7 +45,7 @@ export const Checkbox = ({
|
||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
<label className="text-sm" htmlFor={id}>
|
||||
<label className="text-sm whitespace-nowrap" htmlFor={id}>
|
||||
{children}
|
||||
{isRequired && <span className="pl-1 text-red">*</span>}
|
||||
</label>
|
||||
|
||||
@@ -15,40 +15,16 @@ export type TRole<T extends string | undefined> = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TPermission = TWorkspacePermission | TGeneralPermission;
|
||||
|
||||
type TGeneralPermission = {
|
||||
export type TPermission = {
|
||||
conditions?: Record<string, any>;
|
||||
action: "read" | "edit" | "create" | "delete";
|
||||
subject: "member" | "role" | "incident-contact" | "sso" | "billing" | "settings";
|
||||
action: string;
|
||||
subject: string;
|
||||
};
|
||||
|
||||
type TWorkspacePermission = {
|
||||
export type TProjectPermission = {
|
||||
conditions?: Record<string, any>;
|
||||
action: "read" | "create";
|
||||
subject: "workspace";
|
||||
};
|
||||
|
||||
export type TProjectPermission = TProjectGeneralPermission | TProjectWorkspacePermission;
|
||||
|
||||
type TProjectGeneralPermission = {
|
||||
conditions?: Record<string, any>;
|
||||
action: "read" | "edit" | "create" | "delete";
|
||||
subject:
|
||||
| "member"
|
||||
| "role"
|
||||
| "settings"
|
||||
| "secrets"
|
||||
| "environments"
|
||||
| "folders"
|
||||
| "secret-imports"
|
||||
| "service-tokens";
|
||||
};
|
||||
|
||||
type TProjectWorkspacePermission = {
|
||||
conditions?: Record<string, any>;
|
||||
action: "delete" | "edit";
|
||||
subject: "workspace";
|
||||
action: string;
|
||||
subject: string;
|
||||
};
|
||||
|
||||
export type TCreateRoleDTO<T extends string | undefined> = {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
@@ -50,7 +51,14 @@ import {
|
||||
SelectItem,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription,
|
||||
useUser,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
fetchOrgUsers,
|
||||
@@ -395,11 +403,17 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</div>
|
||||
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
||||
<div className="w-full">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.Workspace}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => {
|
||||
if (isAddingProjectsAllowed) {
|
||||
handlePopUpOpen("addNewWs");
|
||||
@@ -411,6 +425,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
>
|
||||
Add Project
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ const SettingsBilling = withPermission<{}, TOrgPermission>(
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgPermissionActions.Delete, subject: OrgPermissionSubjects.Billing }
|
||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
|
||||
);
|
||||
|
||||
Object.assign(SettingsBilling, { requireAuth: true });
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
html {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
|
||||
.rdp-day,
|
||||
.rdp-nav_button {
|
||||
@apply rounded-md hover:text-mineshaft-500;
|
||||
@@ -108,7 +112,13 @@
|
||||
}
|
||||
}
|
||||
.tags-conic-bg {
|
||||
background: conic-gradient(rgb(235, 87, 87), rgb(242, 201, 76), rgb(76, 183, 130), rgb(78, 167, 252), rgb(250, 96, 122));
|
||||
background: conic-gradient(
|
||||
rgb(235, 87, 87),
|
||||
rgb(242, 201, 76),
|
||||
rgb(76, 183, 130),
|
||||
rgb(78, 167, 252),
|
||||
rgb(250, 96, 122)
|
||||
);
|
||||
}
|
||||
|
||||
.show-tags {
|
||||
|
||||
@@ -1138,7 +1138,6 @@ export const DashboardPage = withProjectPermission(
|
||||
onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)}
|
||||
/>
|
||||
</FormProvider>
|
||||
|
||||
<SecretDropzone
|
||||
workspaceId={workspaceId}
|
||||
isSmaller={!isEmptyPage}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
||||
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
||||
import {
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
|
||||
import { useGetProjectSecrets } from "@app/hooks/api";
|
||||
import { UserWsKeyPair } from "@app/hooks/api/types";
|
||||
@@ -80,8 +80,7 @@ type Props = {
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export const SecretDropzone = withProjectPermission(
|
||||
({
|
||||
export const SecretDropzone = ({
|
||||
isSmaller,
|
||||
onParsedEnv,
|
||||
onAddNewSecret,
|
||||
@@ -226,12 +225,7 @@ export const SecretDropzone = withProjectPermission(
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="mb-16 flex items-center justify-center pt-16">
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="loading animation"
|
||||
/>
|
||||
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
@@ -242,13 +236,21 @@ export const SecretDropzone = withProjectPermission(
|
||||
<div>
|
||||
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<input
|
||||
id="fileSelect"
|
||||
disabled={!isAllowed}
|
||||
type="file"
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
accept=".txt,.env,.yml,.yaml,.json"
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex w-full flex-row items-center justify-center py-4",
|
||||
@@ -269,9 +271,16 @@ export const SecretDropzone = withProjectPermission(
|
||||
}}
|
||||
>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="star" size={isSmaller ? "xs" : "sm"}>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button isDisabled={!isAllowed} variant="star" size={isSmaller ? "xs" : "sm"}>
|
||||
Copy Secrets From An Environment
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</ModalTrigger>
|
||||
<ModalContent
|
||||
className="max-w-2xl"
|
||||
@@ -369,9 +378,7 @@ export const SecretDropzone = withProjectPermission(
|
||||
<Checkbox
|
||||
id={`pull-secret-${_id}`}
|
||||
isChecked={Boolean(value)}
|
||||
onCheckedChange={(isChecked) =>
|
||||
onChange(isChecked ? secVal : "")
|
||||
}
|
||||
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
|
||||
>
|
||||
{key}
|
||||
</Checkbox>
|
||||
@@ -407,16 +414,21 @@ export const SecretDropzone = withProjectPermission(
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isSmaller && (
|
||||
<Button variant="star" onClick={onAddNewSecret}>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Secrets}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button variant="star" onClick={onAddNewSecret} isDisabled={!isAllowed}>
|
||||
Add a new secret
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Create, subject: ProjectPermissionSub.Secrets }
|
||||
);
|
||||
};
|
||||
|
||||
+12
-18
@@ -16,7 +16,8 @@ export const formSchema = z.object({
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
slug: z.string().trim(),
|
||||
permissions: z.object({
|
||||
permissions: z
|
||||
.object({
|
||||
workspace: z
|
||||
.object({
|
||||
read: z.boolean().optional(),
|
||||
@@ -32,21 +33,20 @@ export const formSchema = z.object({
|
||||
sso: generalPermissionSchema,
|
||||
billing: generalPermissionSchema
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
export type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
// convert role permission to form compatiable data structure
|
||||
export const rolePermission2Form = (permissions: TPermission[] = []) => {
|
||||
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||
|
||||
// any because if it set it as form type due to the discriminated union type of ts
|
||||
// i would have to write a if loop with both conditions same
|
||||
const formVal: Record<string, any> = {};
|
||||
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 = subject as keyof TFormSchema["permissions"];
|
||||
(formVal[key] as Exclude<TFormSchema["permissions"]["member"], undefined>)[action] = true;
|
||||
formVal[subject][action] = true;
|
||||
});
|
||||
|
||||
return formVal;
|
||||
@@ -54,19 +54,13 @@ export const rolePermission2Form = (permissions: TPermission[] = []) => {
|
||||
|
||||
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||
const permissions: TPermission[] = [];
|
||||
(Object.keys(formVal) as Array<keyof typeof formVal>).forEach((rule) => {
|
||||
// all these type annotations are due to Object.keys of ts cannot infer and put it just a string[]
|
||||
// quite annoying i know
|
||||
const actions = Object.keys(formVal[rule] || {}) as Array<
|
||||
keyof z.infer<typeof generalPermissionSchema>
|
||||
>;
|
||||
|
||||
actions.forEach((action) => {
|
||||
// akhilmhdh: set it as any due to the union type bug i would end up writing an if else with same condition on both side
|
||||
if (formVal?.[rule]?.[action as keyof typeof formVal.workspace]) {
|
||||
permissions.push({ subject: rule, action } as any);
|
||||
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||
if (isAllowed) {
|
||||
permissions.push({ subject: rule, action });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return permissions;
|
||||
};
|
||||
|
||||
+27
-2
@@ -11,7 +11,7 @@ import { useToggle } from "@app/hooks";
|
||||
import { TFormSchema } from "./OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
formName: keyof Omit<TFormSchema["permissions"], "workspace">;
|
||||
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
|
||||
isNonEditable?: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
@@ -34,6 +34,31 @@ const PERMISSIONS = [
|
||||
{ action: "delete", label: "Remove" }
|
||||
] as const;
|
||||
|
||||
const SECRET_SCANNING_PERMISSIONS = [
|
||||
{ action: "read", label: "View risks" },
|
||||
{ action: "create", label: "Add integrations" },
|
||||
{ action: "edit", label: "Edit risk status" },
|
||||
{ action: "delete", label: "Remove integrations" }
|
||||
] as const;
|
||||
|
||||
const BILLING_PERMISSIONS = [
|
||||
{ action: "read", label: "View bills" },
|
||||
{ action: "create", label: "Add payment methods" },
|
||||
{ action: "edit", label: "Edit payments" },
|
||||
{ action: "delete", label: "Remove payments" }
|
||||
] as const;
|
||||
|
||||
const getPermissionList = (option: Props["formName"]) => {
|
||||
switch (option) {
|
||||
case "secret-scanning":
|
||||
return SECRET_SCANNING_PERMISSIONS;
|
||||
case "billing":
|
||||
return BILLING_PERMISSIONS;
|
||||
default:
|
||||
return PERMISSIONS;
|
||||
}
|
||||
};
|
||||
|
||||
export const SimpleLevelPermissionOption = ({
|
||||
isNonEditable,
|
||||
setValue,
|
||||
@@ -138,7 +163,7 @@ export const SimpleLevelPermissionOption = ({
|
||||
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
|
||||
>
|
||||
{isCustom &&
|
||||
PERMISSIONS.map(({ action, label }) => (
|
||||
getPermissionList(formName).map(({ action, label }) => (
|
||||
<Controller
|
||||
name={`permissions.${formName}.${action}`}
|
||||
key={`permissions.${formName}.${action}`}
|
||||
|
||||
+3
-1
@@ -86,7 +86,9 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col">
|
||||
<div className="font-medium mb-1 text-lg">Project</div>
|
||||
<div className="text-xs font-light">Project management control</div>
|
||||
<div className="text-xs font-light">
|
||||
More fine granined project access control can be defined with project level roles
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
@@ -16,10 +17,9 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteRole } from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
@@ -65,9 +65,17 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
|
||||
placeholder="Search roles..."
|
||||
/>
|
||||
</div>
|
||||
<Button leftIcon={<FontAwesomeIcon icon={faPlus} />} onClick={() => onSelectRole()}>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => onSelectRole()}
|
||||
>
|
||||
Add Role
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
@@ -95,27 +103,42 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex space-x-2 items-center">
|
||||
<Tooltip content="Edit">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Role}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="edit"
|
||||
onClick={() => onSelectRole(role)}
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={isNonMutatable ? "Reserved roles are non-removable" : "Delete"}
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Role}
|
||||
renderTooltip
|
||||
allowedLabel={
|
||||
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
|
||||
}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
onClick={() => handlePopUpOpen("deleteRole", role)}
|
||||
variant="plain"
|
||||
isDisabled={isNonMutatable}
|
||||
isDisabled={isNonMutatable || !isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
+5
-12
@@ -17,7 +17,6 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
@@ -116,10 +115,10 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Role}
|
||||
renderTooltip
|
||||
allowedLabel="Edit"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip content="Edit">
|
||||
<IconButton
|
||||
isDisabled={!isAllowed}
|
||||
ariaLabel="edit"
|
||||
@@ -128,21 +127,17 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Role}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Tooltip
|
||||
content={
|
||||
renderTooltip
|
||||
allowedLabel={
|
||||
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
|
||||
}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
onClick={() => handlePopUpOpen("deleteRole", role)}
|
||||
@@ -151,8 +146,6 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
|
||||
+17
-25
@@ -29,7 +29,8 @@ export const formSchema = z.object({
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
slug: z.string().trim(),
|
||||
permissions: z.object({
|
||||
permissions: z
|
||||
.object({
|
||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
||||
folders: z.record(multiEnvPermissionSchema).optional(),
|
||||
"secret-imports": z.record(multiEnvPermissionSchema).optional(),
|
||||
@@ -56,12 +57,13 @@ export const formSchema = z.object({
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
export type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
const multiEnvApi2Form = (
|
||||
formVal: TFormSchema["permissions"]["secrets"],
|
||||
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
||||
permission: TProjectPermission
|
||||
) => {
|
||||
const isCustomRule = Boolean(permission?.conditions?.environment);
|
||||
@@ -76,28 +78,26 @@ const multiEnvApi2Form = (
|
||||
if (formVal && !formVal?.[secretEnv]) {
|
||||
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
|
||||
}
|
||||
formVal![secretEnv]![permission.action] = true;
|
||||
|
||||
formVal[secretEnv][permission.action] = true;
|
||||
};
|
||||
|
||||
// convert role permission to form compatiable data structure
|
||||
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||
// any because if it set it as form type due to the discriminated union type of ts
|
||||
// i would have to write a if loop with both conditions same
|
||||
const formVal: Record<string, any> = {};
|
||||
|
||||
permissions.forEach((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);
|
||||
multiEnvApi2Form(formVal[subject], permission);
|
||||
} else {
|
||||
// everything else follows same pattern
|
||||
// formVal[settings][read | write] = true
|
||||
formVal[
|
||||
subject as keyof Omit<
|
||||
TFormSchema["permissions"],
|
||||
"secrets" | "workspace" | "secret-rollback"
|
||||
>
|
||||
]![action] = true;
|
||||
formVal[subject][action] = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
|
||||
const multiEnvForm2Api = (
|
||||
permissions: TProjectPermission[],
|
||||
formVal: TFormSchema["permissions"]["secrets"],
|
||||
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
||||
subject: (typeof MULTI_ENV_KEY)[number]
|
||||
) => {
|
||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
||||
@@ -142,22 +142,14 @@ const multiEnvForm2Api = (
|
||||
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||
const permissions: TProjectPermission[] = [];
|
||||
MULTI_ENV_KEY.forEach((formName) => {
|
||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(formVal[formName] || {})), formName);
|
||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(formVal?.[formName] || {})), formName);
|
||||
});
|
||||
// other than workspace everything else follows same
|
||||
// if in future there is a different follow the above on how workspace is done
|
||||
(Object.keys(formVal) as Array<keyof typeof formVal>)
|
||||
.filter((key) => !["secret-imports", "folders", "secrets"].includes(key))
|
||||
.forEach((rule) => {
|
||||
// all these type annotations are due to Object.keys of ts cannot infer and put it just a string[]
|
||||
// quite annoying i know
|
||||
const actions = Object.keys(formVal[rule] || {}) as Array<
|
||||
keyof z.infer<typeof generalPermissionSchema>
|
||||
>;
|
||||
actions.forEach((action) => {
|
||||
// akhilmhdh: set it as any due to the union type bug i would end up writing an if else with same condition on both side
|
||||
if (formVal[rule]?.[action as keyof typeof formVal.workspace]) {
|
||||
permissions.push({ subject: rule, action } as any);
|
||||
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||
if (isAllowed) {
|
||||
permissions.push({ subject: rule, action });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+4
-4
@@ -23,8 +23,8 @@ enum Permission {
|
||||
}
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ action: "edit", label: "Update" },
|
||||
{ action: "delete", label: "Remove" }
|
||||
{ action: "edit", label: "Update workspace details" },
|
||||
{ action: "delete", label: "Delete workspace" }
|
||||
] as const;
|
||||
|
||||
export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
@@ -80,8 +80,8 @@ export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props)
|
||||
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col">
|
||||
<div className="font-medium mb-1 text-lg">Workspace</div>
|
||||
<div className="text-xs font-light">Workspace control actions</div>
|
||||
<div className="font-medium mb-1 text-lg">Project</div>
|
||||
<div className="text-xs font-light">Project control actions</div>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
|
||||
import { OrgNameChangeSection } from "../OrgNameChangeSection";
|
||||
import { OrgServiceAccountsTable } from "../OrgServiceAccountsTable";
|
||||
|
||||
export const OrgGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<OrgNameChangeSection />
|
||||
<OrgServiceAccountsTable />
|
||||
<OrgIncidentContactsSection />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user