feat: renamed old frontend and fixed some minor bugs

This commit is contained in:
=
2025-01-06 14:56:49 +05:30
parent 8c902d7699
commit 54377a44d3
2577 changed files with 35240 additions and 35116 deletions
@@ -0,0 +1,176 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
position?: number;
isDisabled?: boolean;
type:
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports;
};
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${type}.${position}.conditions`
});
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
<p className="mb-2 text-sm text-mineshaft-400">
When this policy should apply (always if no conditions are added).
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${type}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[type]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[type]?.[position]?.conditions?.message}</span>
</div>
)}
<div>{}</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
);
};
@@ -0,0 +1,188 @@
import { cloneElement } from "react";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import {
faChevronDown,
faChevronRight,
faInfoCircle,
faPlus,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Button, Checkbox, Select, SelectItem, Tag, Tooltip } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import {
isConditionalSubjects,
TFormSchema,
TProjectPermissionObject
} from "./ProjectRoleModifySection.utils";
type Props<T extends ProjectPermissionSub> = {
title: string;
subject: T;
actions: TProjectPermissionObject[T]["actions"];
children?: JSX.Element;
isDisabled?: boolean;
};
export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
subject,
actions,
children,
title,
isDisabled
}: Props<T>) => {
const { control } = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${subject}`
});
const [isOpen, setIsOpen] = useToggle();
if (!items.fields.length) return <div />;
return (
<div className="border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md">
<div
className="flex cursor-pointer items-center space-x-8 px-5 py-4 text-sm text-gray-300"
role="button"
tabIndex={0}
onClick={() => setIsOpen.toggle()}
onKeyDown={(e) => {
if (e.key === "Enter") {
setIsOpen.toggle();
}
}}
>
<div>
<FontAwesomeIcon icon={isOpen ? faChevronDown : faChevronRight} />
</div>
<div className="flex-grow">{title}</div>
{items.fields.length > 1 && (
<div>
<Tag size="xs" className="px-2">
{items.fields.length} rules
</Tag>
</div>
)}
</div>
{isOpen && (
<div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6">
{items.fields.map((el, rootIndex) => (
<div key={el.id} className="bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md">
{isConditionalSubjects(subject) && (
<div className="mb-6 mt-4 flex w-full items-center text-gray-300">
<div className="w-1/4">Permission</div>
<div className="mr-4 w-1/4">
<Controller
defaultValue={false as any}
name={`permissions.${subject}.${rootIndex}.inverted`}
render={({ field }) => (
<Select
value={String(field.value)}
onValueChange={(val) => field.onChange(val === "true")}
containerClassName="w-full"
className="w-full"
isDisabled={isDisabled}
>
<SelectItem value="false">Allow</SelectItem>
<SelectItem value="true">Forbid</SelectItem>
</Select>
)}
/>
</div>
<div>
<Tooltip
asChild
content={
<>
<p>
Whether to allow or forbid the selected actions when the following
conditions (if any) are met.
</p>
<p className="mt-2">Forbid rules must come after allow rules.</p>
</>
}
>
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="text-gray-400" />
</Tooltip>
</div>
</div>
)}
<div className="flex text-gray-300">
<div className="w-1/4">Actions</div>
<div className="flex flex-grow flex-wrap justify-start gap-8">
{actions.map(({ label, value }) => {
if (typeof value !== "string") return undefined;
return (
<Controller
key={`${el.id}-${label}`}
name={`permissions.${subject}.${rootIndex}.${value}` as any}
control={control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isDisabled={isDisabled}
isChecked={Boolean(field.value)}
onCheckedChange={field.onChange}
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
>
{label}
</Checkbox>
</div>
)}
/>
);
})}
</div>
</div>
{children &&
cloneElement(children, {
position: rootIndex
})}
<div
className={twMerge(
"mt-4 flex justify-start space-x-4",
isConditionalSubjects(subject) && "justify-end"
)}
>
{!isDisabled && isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-2"
onClick={() => {
items.insert(rootIndex + 1, [
{ read: false, edit: false, create: false, delete: false } as any
]);
}}
isDisabled={isDisabled}
>
Add policy
</Button>
)}
{!isDisabled && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="mt-2 hover:border-red"
onClick={() => items.remove(rootIndex)}
isDisabled={isDisabled}
>
Remove policy
</Button>
)}{" "}
</div>
</div>
))}
</div>
)}
</div>
);
};
@@ -0,0 +1,171 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const IdentityManagementPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.Identity;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
<p className="mb-2 text-sm text-mineshaft-400">
When this policy should apply (always if no conditions are added).
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="identityId">Identity ID</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>{}</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "identityId",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
);
};
@@ -0,0 +1,124 @@
import { Controller, useForm, useFormContext } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
Checkbox,
FormControl,
FormLabel,
ModalClose,
Select,
SelectItem
} from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import {
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema,
TFormSchema
} from "./ProjectRoleModifySection.utils";
type Props = {
onClose: () => void;
};
export const NewPermissionRule = ({ onClose }: Props) => {
const rootForm = useFormContext<TFormSchema>();
const form = useForm<{
type: ProjectPermissionSub;
permissions: NonNullable<TFormSchema["permissions"]>;
}>({
resolver: zodResolver(
projectRoleFormSchema
.pick({ permissions: true })
.extend({ type: z.nativeEnum(ProjectPermissionSub) })
),
defaultValues: {
type: ProjectPermissionSub.Secrets
}
});
const selectedSubject = form.watch("type");
return (
<div>
<Controller
control={form.control}
name="type"
defaultValue={ProjectPermissionSub.Secrets}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Subject" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{Object.keys(PROJECT_PERMISSION_OBJECT).map((subject) => (
<SelectItem value={subject} key={`permission-create-${subject}`}>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<FormLabel label="Actions" className="my-2" />
<div className="flex flex-grow flex-wrap justify-start gap-8">
{PROJECT_PERMISSION_OBJECT?.[selectedSubject]?.actions?.map(({ label, value }) => (
<Controller
key={`create-permission-${selectedSubject}-${label}`}
name={`permissions.${selectedSubject}.0.${value as any}` as any}
control={form.control}
defaultValue={false}
render={({ field }) => (
<div className="flex items-center justify-center">
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`new-permissions.${selectedSubject}.0.${String(value)}`}
>
{label}
</Checkbox>
</div>
)}
/>
))}
</div>
<div className="mt-8 flex space-x-4">
<Button
onClick={form.handleSubmit((el) => {
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
rootForm.setValue(
`permissions.${el.type}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...(el?.permissions[el.type] || [])],
{ shouldDirty: true, shouldTouch: true }
);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
rootForm.setValue(`permissions.${el.type}`, el?.permissions?.[el.type], {
shouldDirty: true,
shouldTouch: true
});
}
onClose();
})}
>
Create
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</div>
);
};
@@ -0,0 +1,33 @@
import { GlobPermissionInfo } from "@app/components/permissions";
import { SelectItem } from "@app/components/v2";
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
export const getConditionOperatorHelperInfo = (type: PermissionConditionOperators) => {
switch (type) {
case PermissionConditionOperators.$EQ:
return "Value should equal specified value.";
case PermissionConditionOperators.$NEQ:
return "Value should not equal specified value.";
case PermissionConditionOperators.$IN:
return "List of comma-separated values that match a given value.";
case PermissionConditionOperators.$GLOB:
return <GlobPermissionInfo />;
default:
return "";
}
};
export const renderOperatorSelectItems = (type: string) => {
if (type === "secretTags") {
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
}
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</>
);
};
@@ -0,0 +1,18 @@
import { useFormContext } from "react-hook-form";
import { EmptyState } from "@app/components/v2";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
// This is made into seperate component because watch subscribes to all permissions
// thus keeping in top level casues render on all ones
export const PermissionEmptyState = () => {
const { watch } = useFormContext<TFormSchema>();
const isNotEmptyPermissions = Object.entries(watch("permissions") || {}).some(
([key, value]) => key && value?.length > 0
);
if (isNotEmptyPermissions) return <div />;
return <EmptyState title="No policies applied" className="py-8" />;
};
@@ -0,0 +1,674 @@
import { z } from "zod";
import {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionSub
} from "@app/context";
import {
PermissionConditionOperators,
ProjectPermissionDynamicSecretActions,
TPermissionCondition,
TPermissionConditionOperators
} from "@app/context/ProjectPermissionContext/types";
import { TProjectPermission } from "@app/hooks/api/roles/types";
const GeneralPolicyActionSchema = z.object({
read: z.boolean().optional(),
edit: z.boolean().optional(),
delete: z.boolean().optional(),
create: z.boolean().optional()
});
const CmekPolicyActionSchema = z.object({
read: z.boolean().optional(),
edit: z.boolean().optional(),
delete: z.boolean().optional(),
create: z.boolean().optional(),
encrypt: z.boolean().optional(),
decrypt: z.boolean().optional()
});
const DynamicSecretPolicyActionSchema = z.object({
[ProjectPermissionDynamicSecretActions.ReadRootCredential]: z.boolean().optional(),
[ProjectPermissionDynamicSecretActions.EditRootCredential]: z.boolean().optional(),
[ProjectPermissionDynamicSecretActions.DeleteRootCredential]: z.boolean().optional(),
[ProjectPermissionDynamicSecretActions.CreateRootCredential]: z.boolean().optional(),
[ProjectPermissionDynamicSecretActions.Lease]: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({
read: z.boolean().optional(),
create: z.boolean().optional()
});
const WorkspacePolicyActionSchema = z.object({
edit: z.boolean().optional(),
delete: z.boolean().optional()
});
const ConditionSchema = z
.object({
operator: z.string(),
lhs: z.string(),
rhs: z.string().min(1)
})
.array()
.optional()
.default([])
.refine(
(el) => {
const lhsOperatorSet = new Set<string>();
for (let i = 0; i < el.length; i += 1) {
const { lhs, operator } = el[i];
if (lhsOperatorSet.has(`${lhs}-${operator}`)) {
return false;
}
lhsOperatorSet.add(`${lhs}-${operator}`);
}
return true;
},
{ message: "Duplicate operator found for a condition" }
);
export const projectRoleFormSchema = z.object({
name: z.string().trim(),
description: z.string().trim().optional(),
slug: z
.string()
.trim()
.toLowerCase()
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
permissions: z
.object({
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SecretFolders]: GeneralPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SecretImports]: GeneralPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.DynamicSecrets]: DynamicSecretPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Integrations]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Webhooks]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.ServiceTokens]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Settings]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default(
[]
),
[ProjectPermissionSub.SshCertificates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SshCertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Cmek]: CmekPolicyActionSchema.array().default([])
})
.partial()
.optional()
});
export type TFormSchema = z.infer<typeof projectRoleFormSchema>;
type TConditionalFields =
| ProjectPermissionSub.Secrets
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.Identity;
export const isConditionalSubjects = (
subject: ProjectPermissionSub
): subject is TConditionalFields =>
subject === (ProjectPermissionSub.Secrets as const) ||
subject === ProjectPermissionSub.DynamicSecrets ||
subject === ProjectPermissionSub.SecretImports ||
subject === ProjectPermissionSub.SecretFolders ||
subject === ProjectPermissionSub.Identity;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema> = [];
Object.entries(caslConditions).forEach(([type, condition]) => {
if (typeof condition === "string") {
formConditions.push({
operator: PermissionConditionOperators.$EQ,
lhs: type,
rhs: condition
});
} else {
Object.keys(condition).forEach((conditionOperator) => {
const rhs = condition[conditionOperator as PermissionConditionOperators];
formConditions.push({
operator: conditionOperator,
lhs: type,
rhs: typeof rhs === "string" ? rhs : rhs.join(",")
});
});
}
});
return formConditions;
};
// convert role permission to form compatible data structure
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const formVal: Partial<TFormSchema["permissions"]> = {};
permissions.forEach((permission) => {
const { subject: caslSub, action, conditions, inverted } = permission;
const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub;
if (!action.length) return;
if (
[
ProjectPermissionSub.Secrets,
ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.Member,
ProjectPermissionSub.Groups,
ProjectPermissionSub.Identity,
ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks,
ProjectPermissionSub.ServiceTokens,
ProjectPermissionSub.Settings,
ProjectPermissionSub.Environments,
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.Certificates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.Tags,
ProjectPermissionSub.SecretRotation,
ProjectPermissionSub.Kms
].includes(subject)
) {
// from above statement we are sure it won't be undefined
if (isConditionalSubjects(subject)) {
if (!formVal[subject]) formVal[subject] = [];
if (subject === ProjectPermissionSub.DynamicSecrets) {
const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential);
const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential);
const canDelete = action.includes(
ProjectPermissionDynamicSecretActions.DeleteRootCredential
);
const canCreate = action.includes(
ProjectPermissionDynamicSecretActions.CreateRootCredential
);
const canLease = action.includes(ProjectPermissionDynamicSecretActions.Lease);
// from above statement we are sure it won't be undefined
formVal[subject]!.push({
[ProjectPermissionDynamicSecretActions.ReadRootCredential]: canRead,
[ProjectPermissionDynamicSecretActions.CreateRootCredential]: canCreate,
[ProjectPermissionDynamicSecretActions.EditRootCredential]: canEdit,
[ProjectPermissionDynamicSecretActions.DeleteRootCredential]: canDelete,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted,
[ProjectPermissionDynamicSecretActions.Lease]: canLease
});
return;
}
// for other subjects
const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit);
const canDelete = action.includes(ProjectPermissionActions.Delete);
const canCreate = action.includes(ProjectPermissionActions.Create);
// remove this condition later
// keeping when old routes create permission with folder read
if (
subject === ProjectPermissionSub.SecretFolders &&
canRead &&
!canEdit &&
!canDelete &&
!canCreate
) {
return;
}
formVal[subject]!.push({
read: canRead,
create: canCreate,
edit: canEdit,
delete: canDelete,
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
return;
}
// deduplicate multiple rules for other policies
// because they don't have condition it doesn't make sense for multiple rules
const canRead = action.includes(ProjectPermissionActions.Read);
const canEdit = action.includes(ProjectPermissionActions.Edit);
const canDelete = action.includes(ProjectPermissionActions.Delete);
const canCreate = action.includes(ProjectPermissionActions.Create);
if (!formVal[subject]) formVal[subject] = [{}];
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true;
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
return;
}
if (subject === ProjectPermissionSub.Project) {
const canEdit = action.includes(ProjectPermissionActions.Edit);
const canDelete = action.includes(ProjectPermissionActions.Delete);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canEdit) formVal[subject as ProjectPermissionSub.Project]![0].edit = true;
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
return;
}
if (subject === ProjectPermissionSub.SecretRollback) {
const canRead = action.includes(ProjectPermissionActions.Read);
const canCreate = action.includes(ProjectPermissionActions.Create);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
return;
}
if (subject === ProjectPermissionSub.Cmek) {
const canRead = action.includes(ProjectPermissionCmekActions.Read);
const canEdit = action.includes(ProjectPermissionCmekActions.Edit);
const canDelete = action.includes(ProjectPermissionCmekActions.Delete);
const canCreate = action.includes(ProjectPermissionCmekActions.Create);
const canEncrypt = action.includes(ProjectPermissionCmekActions.Encrypt);
const canDecrypt = action.includes(ProjectPermissionCmekActions.Decrypt);
if (!formVal[subject]) formVal[subject] = [{}];
// from above statement we are sure it won't be undefined
if (canRead) formVal[subject]![0].read = true;
if (canEdit) formVal[subject]![0].edit = true;
if (canCreate) formVal[subject]![0].create = true;
if (canDelete) formVal[subject]![0].delete = true;
if (canEncrypt) formVal[subject]![0].encrypt = true;
if (canDecrypt) formVal[subject]![0].decrypt = true;
}
});
return formVal;
};
const convertFormOperatorToCaslCondition = (
conditions: { lhs: string; rhs: string; operator: string }[]
) => {
const caslCondition: Record<string, Partial<TPermissionConditionOperators>> = {};
conditions.forEach((el) => {
if (!caslCondition[el.lhs]) caslCondition[el.lhs] = {};
if (
el.operator === PermissionConditionOperators.$IN ||
el.operator === PermissionConditionOperators.$ALL
) {
caslCondition[el.lhs][el.operator] = el.rhs.split(",");
} else {
caslCondition[el.lhs][
el.operator as Exclude<
PermissionConditionOperators,
PermissionConditionOperators.$ALL | PermissionConditionOperators.$IN
>
] = el.rhs;
}
});
return caslCondition;
};
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
const permissions: TProjectPermission[] = [];
// other than workspace everything else follows same
// if in future there is a different follow the above on how workspace is done
Object.entries(formVal || {}).forEach(([subject, rules]) => {
rules.forEach((actions) => {
const caslActions = Object.keys(actions).filter(
(el) => actions?.[el as keyof typeof actions] && el !== "conditions" && el !== "inverted"
);
const caslConditions =
"conditions" in actions
? convertFormOperatorToCaslCondition(actions.conditions)
: undefined;
permissions.push({
action: caslActions,
subject,
inverted: (actions as { inverted?: boolean })?.inverted,
conditions: caslConditions
});
});
});
return permissions;
};
export type TProjectPermissionObject = {
[K in ProjectPermissionSub]: {
title: string;
actions: {
label: string;
value: keyof Omit<
NonNullable<NonNullable<TFormSchema["permissions"]>[K]>[number],
"conditions" | "inverted"
>;
}[];
};
};
export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
[ProjectPermissionSub.Secrets]: {
title: "Secrets",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SecretFolders]: {
title: "Secret Folders",
actions: [
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SecretImports]: {
title: "Secret Imports",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.DynamicSecrets]: {
title: "Dynamic Secrets",
actions: [
{
label: "Read root credentials",
value: ProjectPermissionDynamicSecretActions.ReadRootCredential
},
{
label: "Create root credentials",
value: ProjectPermissionDynamicSecretActions.CreateRootCredential
},
{
label: "Modify root credentials",
value: ProjectPermissionDynamicSecretActions.EditRootCredential
},
{
label: "Remove root credentials",
value: ProjectPermissionDynamicSecretActions.DeleteRootCredential
},
{ label: "Manage Leases", value: ProjectPermissionDynamicSecretActions.Lease }
]
},
[ProjectPermissionSub.Cmek]: {
title: "KMS",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" },
{ label: "Encrypt", value: "encrypt" },
{ label: "Decrypt", value: "decrypt" }
]
},
[ProjectPermissionSub.Kms]: {
title: "Project KMS Configuration",
actions: [{ label: "Modify", value: "edit" }]
},
[ProjectPermissionSub.Integrations]: {
title: "Integrations",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.Project]: {
title: "Project",
actions: [
{ label: "Update project details", value: "edit" },
{ label: "Delete project", value: "delete" }
]
},
[ProjectPermissionSub.Role]: {
title: "Roles",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.Member]: {
title: "User Management",
actions: [
{ label: "View all members", value: "read" },
{ label: "Invite members", value: "create" },
{ label: "Edit members", value: "edit" },
{ label: "Remove members", value: "delete" }
]
},
[ProjectPermissionSub.Identity]: {
title: "Machine Identity Management",
actions: [
{ label: "Read", value: "read" },
{ label: "Add", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.Groups]: {
title: "Group Management",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.Webhooks]: {
title: "Webhooks",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.ServiceTokens]: {
title: "Service Tokens",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.Settings]: {
title: "Settings",
actions: [
{ label: "Read", value: "read" },
{ label: "Modify", value: "edit" }
]
},
[ProjectPermissionSub.Environments]: {
title: "Environment Management",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.Tags]: {
title: "Tags",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.AuditLogs]: {
title: "Audit Logs",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.IpAllowList]: {
title: "IP Allowlist",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.CertificateAuthorities]: {
title: "Certificate Authorities",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.Certificates]: {
title: "Certificates",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.CertificateTemplates]: {
title: "Certificate Templates",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SshCertificateAuthorities]: {
title: "SSH Certificate Authorities",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SshCertificates]: {
title: "SSH Certificates",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SshCertificateTemplates]: {
title: "SSH Certificate Templates",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.PkiCollections]: {
title: "PKI Collections",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.PkiAlerts]: {
title: "PKI Alerts",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SecretApproval]: {
title: "Secret Protect policy",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SecretRotation]: {
title: "Secret Rotation",
actions: [
{ label: "Read", value: "read" },
{ label: "Create", value: "create" },
{ label: "Modify", value: "edit" },
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SecretRollback]: {
title: "Secret Rollback",
actions: [
{ label: "Perform rollback", value: "create" },
{ label: "View", value: "read" }
]
}
};
@@ -0,0 +1,95 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetProjectRoleBySlug } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
roleSlug: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: object) => void;
};
export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { currentWorkspace } = useWorkspace();
const { data } = useGetProjectRoleBySlug(currentWorkspace?.id ?? "", roleSlug as string);
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(data?.slug ?? "");
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Project Role Details</h3>
{isCustomRole && (
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Role}>
{(isAllowed) => {
return (
<Tooltip content="Edit Role">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("role", {
roleSlug
});
}}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</ProjectPermissionCan>
)}
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Role ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{data.id}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(data.id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Slug</p>
<p className="text-sm text-mineshaft-300">{data.slug}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
<p className="text-sm text-mineshaft-300">
{data.description?.length ? data.description : "-"}
</p>
</div>
</div>
</div>
) : (
<div />
);
};
@@ -0,0 +1,204 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useCreateProjectRole,
useGetProjectRoleBySlug,
useUpdateProjectRole
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas";
const schema = z
.object({
name: z.string(),
description: z.string(),
slug: slugSchema({ min: 1 })
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["role"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["role"]>, state?: boolean) => void;
};
export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
const navigate = useNavigate();
const popupData = popUp?.role?.data as {
roleSlug: string;
};
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data: role } = useGetProjectRoleBySlug(projectId, popupData?.roleSlug ?? "");
const { mutateAsync: createProjectRole } = useCreateProjectRole();
const { mutateAsync: updateProjectRole } = useUpdateProjectRole();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: ""
}
});
useEffect(() => {
if (role) {
reset({
name: role.name,
description: role.description,
slug: role.slug
});
} else {
reset({
name: "",
description: "",
slug: ""
});
}
}, [role]);
const onFormSubmit = async ({ name, description, slug }: FormData) => {
try {
if (!projectId) return;
if (role) {
// update
await updateProjectRole({
id: role.id,
projectId,
name,
description,
slug
});
handlePopUpToggle("role", false);
} else {
// create
const newRole = await createProjectRole({
projectId,
name,
description,
slug,
permissions: []
});
navigate({
to: `/${currentWorkspace.type}/$projectId/roles/$roleSlug` as const,
params: {
roleSlug: newRole.slug,
projectId
}
});
handlePopUpToggle("role", false);
}
createNotification({
text: `Successfully ${popUp?.role?.data ? "updated" : "created"} role`,
type: "success"
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ??
`Failed to ${popUp?.role?.data ? "update" : "create"} role`;
createNotification({
text,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.role?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("role", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.role?.data ? "Update" : "Create"} Role`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="Billing Team" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="slug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Slug"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="billing" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="To manage billing" />
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.role?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("role", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,208 @@
import { FormProvider, useForm } from "react-hook-form";
import { faPlus, faSave } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
import { PermissionEmptyState } from "./PermissionEmptyState";
import {
formRolePermission2API,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema,
rolePermission2Form,
TFormSchema
} from "./ProjectRoleModifySection.utils";
import { SecretPermissionConditions } from "./SecretPermissionConditions";
type Props = {
roleSlug: string;
isDisabled?: boolean;
};
export const renderConditionalComponents = (
subject: ProjectPermissionSub,
isDisabled?: boolean
) => {
if (subject === ProjectPermissionSub.Secrets)
return <SecretPermissionConditions isDisabled={isDisabled} />;
if (isConditionalSubjects(subject)) {
if (subject === ProjectPermissionSub.Identity) {
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
}
return undefined;
};
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data: role, isPending } = useGetProjectRoleBySlug(
currentWorkspace?.id ?? "",
roleSlug as string
);
const form = useForm<TFormSchema>({
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
resolver: zodResolver(projectRoleFormSchema)
});
const {
handleSubmit,
formState: { isDirty, isSubmitting },
reset
} = form;
const { mutateAsync: updateRole } = useUpdateProjectRole();
const onSubmit = async (el: TFormSchema) => {
try {
if (!projectId || !role?.id) return;
await updateRole({
id: role?.id as string,
projectId,
...el,
permissions: formRolePermission2API(el.permissions)
});
createNotification({ type: "success", text: "Successfully updated role" });
} catch (err) {
console.log(err);
createNotification({ type: "error", text: "Failed to update role" });
}
};
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(role?.slug ?? "");
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<FormProvider {...form}>
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
<div className="flex items-center space-x-4">
{isCustomRole && (
<>
{isDirty && (
<Button
className="mr-4 text-mineshaft-300"
variant="link"
isDisabled={isSubmitting}
isLoading={isSubmitting}
onClick={() => reset()}
>
Discard
</Button>
)}
<div className="flex items-center">
<Button
variant="outline_bg"
type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />}
>
Save
</Button>
<DropdownMenu>
<DropdownMenuTrigger>
<Button
isDisabled={isDisabled}
className="h-10 rounded-l-none"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
New policy
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
{Object.keys(PROJECT_PERMISSION_OBJECT)
.sort((a, b) =>
PROJECT_PERMISSION_OBJECT[
a as keyof typeof PROJECT_PERMISSION_OBJECT
].title
.toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((subject) => (
<DropdownMenuItem
key={`permission-create-${subject}`}
className="py-3"
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
)}
</div>
</div>
<div className="py-4">
{!isPending && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
<GeneralPermissionPolicies
subject={subject}
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
title={PROJECT_PERMISSION_OBJECT[subject].title}
key={`project-permission-${subject}`}
isDisabled={isDisabled}
>
{renderConditionalComponents(subject, isDisabled)}
</GeneralPermissionPolicies>
))}
</div>
</FormProvider>
</form>
);
};
@@ -0,0 +1,176 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.secrets.${position}.conditions`
});
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
<p className="mb-2 text-sm text-mineshaft-400">
When this policy should apply (always if no conditions are added).
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition = watch(`permissions.secrets.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.secrets.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
<SelectItem value="secretName">Secret Name</SelectItem>
<SelectItem value="secretTags">Secret Tags</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.secrets?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.secrets?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
);
};