feat(rbac): added glob support in permission and revealed settings

This commit is contained in:
Akhil Mohan
2023-09-08 21:22:36 +05:30
parent 2ed079830a
commit a4ef829046
38 changed files with 2068 additions and 1831 deletions
@@ -1,4 +1,5 @@
import { memo } from "react";
import { subject } from "@casl/ability";
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -9,6 +10,8 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
type Props = {
folders?: Array<{ id: string; name: string }>;
search?: string;
environment: string;
secretPath: string;
onFolderUpdate: (folderId: string, name: string) => void;
onFolderDelete: (folderId: string, name: string) => void;
onFolderOpen: (folderId: string) => void;
@@ -20,7 +23,9 @@ export const FolderSection = memo(
onFolderDelete: handleFolderDelete,
onFolderOpen: handleFolderOpen,
search = "",
folders = []
folders = [],
environment,
secretPath
}: Props) => {
return (
<>
@@ -51,7 +56,7 @@ export const FolderSection = memo(
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Folders}
a={subject(ProjectPermissionSub.Folders, { environment, secretPath })}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
@@ -72,7 +77,7 @@ export const FolderSection = memo(
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Folders}
a={subject(ProjectPermissionSub.Folders, { environment, secretPath })}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
@@ -1,4 +1,5 @@
import { useFormContext, useWatch } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -23,6 +24,8 @@ import { GenRandomNumber } from "./GenRandomNumber";
type Props = {
isDrawerOpen: boolean;
environment: string;
secretPath: string;
onOpenChange: (isOpen: boolean) => void;
index: number;
isReadOnly?: boolean;
@@ -41,7 +44,9 @@ export const SecretDetailDrawer = ({
isReadOnly,
onSecretDelete,
onSave,
onEnvCompare
onEnvCompare,
environment,
secretPath
}: Props): JSX.Element => {
const [canRevealSecVal, setCanRevealSecVal] = useToggle();
const [canRevealSecOverride, setCanRevealSecOverride] = useToggle();
@@ -89,7 +94,7 @@ export const SecretDetailDrawer = ({
<div className="flex w-full space-x-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly || !isAllowed}>
@@ -98,8 +103,8 @@ export const SecretDetailDrawer = ({
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Secrets}
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button
@@ -1,6 +1,7 @@
import { ChangeEvent, DragEvent, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import {
faClone,
@@ -78,6 +79,8 @@ type Props = {
environments?: { name: string; slug: string }[];
workspaceId: string;
decryptFileKey: UserWsKeyPair;
environment: string;
secretPath: string;
};
export const SecretDropzone = ({
@@ -86,7 +89,9 @@ export const SecretDropzone = ({
onAddNewSecret,
environments = [],
workspaceId,
decryptFileKey
decryptFileKey,
environment,
secretPath
}: Props): JSX.Element => {
const { t } = useTranslation();
const [isDragActive, setDragActive] = useToggle();
@@ -109,16 +114,16 @@ export const SecretDropzone = ({
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
});
const secretPath = watch("secretPath");
const envCopySecPath = watch("secretPath");
const selectedEnvSlug = watch("environment");
const debouncedSecretPath = useDebounce(secretPath);
const debouncedEnvCopySecretPath = useDebounce(envCopySecPath);
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
env: selectedEnvSlug,
secretPath: debouncedSecretPath,
secretPath: debouncedEnvCopySecretPath,
isPaused:
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) &&
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedEnvCopySecretPath)) &&
!popUp.importSecEnv.isOpen,
decryptFileKey
});
@@ -126,7 +131,7 @@ export const SecretDropzone = ({
useEffect(() => {
setValue("secrets", {});
setSearchFilter("");
}, [debouncedSecretPath]);
}, [debouncedEnvCopySecretPath]);
const handleDrag = (e: DragEvent) => {
e.preventDefault();
@@ -238,7 +243,7 @@ export const SecretDropzone = ({
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<input
@@ -271,16 +276,22 @@ export const SecretDropzone = ({
}}
>
<ModalTrigger asChild>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<Button isDisabled={!isAllowed} variant="star" size={isSmaller ? "xs" : "sm"}>
Copy Secrets From An Environment
</Button>
)}
</ProjectPermissionCan>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
variant="star"
size={isSmaller ? "xs" : "sm"}
>
Copy Secrets From An Environment
</Button>
)}
</ProjectPermissionCan>
</div>
</ModalTrigger>
<ModalContent
className="max-w-2xl"
@@ -416,7 +427,7 @@ export const SecretDropzone = ({
{!isSmaller && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button variant="star" onClick={onAddNewSecret} isDisabled={!isAllowed}>
@@ -1,4 +1,5 @@
import { useEffect } from "react";
import { subject } from "@casl/ability";
import { useSortable } from "@dnd-kit/sortable";
import {
faFileImport,
@@ -11,11 +12,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub,useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
type Props = {
onDelete: (environment: string, secretPath: string) => void;
environment: string;
secretPath: string;
importedEnv: string;
importedSecPath: string;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
@@ -40,7 +43,9 @@ export const SecretImportItem = ({
importedSecPath,
onDelete,
importedSecrets = [],
searchTerm = ""
searchTerm = "",
secretPath,
environment
}: Props) => {
const [isExpanded, setIsExpanded] = useToggle();
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
@@ -114,7 +119,7 @@ export const SecretImportItem = ({
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SecretImports}
a={subject(ProjectPermissionSub.SecretImports, { environment, secretPath })}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
@@ -59,13 +59,23 @@ export const computeImportedSecretRows = (
type Props = {
secrets?: DecryptedSecret[];
importedSecrets?: TImportedSecrets;
environment: string;
secretPath: string;
onSecretImportDelete: (env: string, secPath: string) => void;
items: { id: string; environment: string; secretPath: string }[];
searchTerm: string;
};
export const SecretImportSection = memo(
({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [], searchTerm = "" }: Props) => {
({
secrets = [],
environment,
secretPath,
importedSecrets = [],
onSecretImportDelete,
items = [],
searchTerm = ""
}: Props) => {
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
@@ -82,6 +92,8 @@ export const SecretImportSection = memo(
secrets,
environments
)}
secretPath={secretPath}
environment={environment}
onDelete={onSecretImportDelete}
importedSecPath={impSecPath}
searchTerm={searchTerm}
@@ -8,6 +8,7 @@ import {
UseFormSetValue,
useWatch
} from "react-hook-form";
import { subject } from "@casl/ability";
import {
faCheck,
faCodeBranch,
@@ -22,31 +23,34 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { cx } from "cva";
import { twMerge } from "tailwind-merge";
// TODO:(akhilmhdh): Refactor this
import AddTagPopoverContent from "@app/components/AddTagPopoverContent/AddTagPopoverContent";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
FormControl,
HoverCard,
HoverCardContent,
HoverCardTrigger,
IconButton,
Input,
Popover,
PopoverContent,
PopoverTrigger,
SecretInput,
Tag,
TextArea,
Tooltip
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { WsTag } from "@app/hooks/api/types";
import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent";
import { FormData, SecretActionType } from "../../DashboardPage.utils";
type Props = {
index: number;
environment: string;
secretPath: string;
// backend generated unique id
secUniqId?: string;
// permission and external state's that decided to hide or show
@@ -74,6 +78,8 @@ type Props = {
export const SecretInputRow = memo(
({
index,
secretPath,
environment,
isSecretValueHidden,
onRowExpand,
isReadOnly,
@@ -84,7 +90,7 @@ export const SecretInputRow = memo(
onSecretDelete,
searchTerm,
control,
// register,
register,
setValue,
isKeyError,
keyError,
@@ -222,7 +228,6 @@ export const SecretInputRow = memo(
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
</td>
<Controller
control={control}
defaultValue=""
@@ -361,16 +366,24 @@ export const SecretInputRow = memo(
<Popover>
<PopoverTrigger asChild>
<div className="w-0 group-hover:w-6 data-[state=open]:w-6">
<Tooltip content="Add tags">
<IconButton
variant="plain"
size="md"
ariaLabel="add-tag"
className="py-[0.42rem]"
>
<FontAwesomeIcon icon={faTags} />
</IconButton>
</Tooltip>
<ProjectPermissionCan
renderTooltip
allowedLabel="Add Tags"
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<IconButton
isDisabled={!isAllowed}
variant="plain"
size="md"
ariaLabel="add-tags"
className="py-[0.42rem]"
>
<FontAwesomeIcon icon={faTags} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
</PopoverTrigger>
<AddTagPopoverContent
@@ -390,55 +403,76 @@ export const SecretInputRow = memo(
<div className="flex h-8 flex-row items-center pr-2">
{!isAddOnly && (
<div>
<Tooltip content="Override with a personal value">
<IconButton
variant="plain"
className={twMerge(
"mt-0.5 w-0 overflow-hidden p-0 group-hover:ml-1 group-hover:w-7",
isOverridden && "ml-1 w-7 text-primary"
)}
onClick={onSecretOverride}
size="md"
isDisabled={isRollbackMode || isReadOnly}
ariaLabel="info"
>
<div className="flex items-center space-x-1">
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
</div>
</IconButton>
</Tooltip>
<ProjectPermissionCan
renderTooltip
allowedLabel="Override with a personal value"
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<IconButton
variant="plain"
className={twMerge(
"mt-0.5 w-0 overflow-hidden p-0 group-hover:ml-1 group-hover:w-7",
isOverridden && "ml-1 w-7 text-primary"
)}
onClick={onSecretOverride}
size="md"
isDisabled={isRollbackMode || isReadOnly || !isAllowed}
ariaLabel="info"
>
<div className="flex items-center space-x-1">
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
</div>
</IconButton>
)}
</ProjectPermissionCan>
</div>
)}
<Tooltip content="Comment">
<div className="mt-0.5 overflow-hidden ">
<Popover>
<PopoverTrigger asChild>
<IconButton
className={twMerge(
"w-7 overflow-hidden p-0",
"w-0 group-hover:w-7 data-[state=open]:w-7",
hasComment ? "w-7 text-primary" : "group-hover:w-7"
)}
variant="plain"
size="md"
ariaLabel="add-tag"
<div className="mt-0.5 overflow-hidden ">
<Popover>
<PopoverTrigger asChild>
<div>
<ProjectPermissionCan
renderTooltip
allowedLabel="Comment"
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
<FontAwesomeIcon icon={faComment} />
</IconButton>
</PopoverTrigger>
<AddTagPopoverContent
wsTags={wsTags}
secKey={secKey || "this secret"}
selectedTagIds={selectedTagIds}
handleSelectTag={(wsTag: WsTag) => onSelectTag(wsTag)}
handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)}
handleTagOnMouseLeave={() => handleTagOnMouseLeave()}
checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)}
handleOnCreateTagOpen={() => onCreateTagOpen()}
/>
</Popover>
</div>
</Tooltip>
{(isAllowed) => (
<IconButton
className={twMerge(
"w-7 overflow-hidden p-0",
"w-0 group-hover:w-7 data-[state=open]:w-7",
hasComment ? "w-7 text-primary" : "group-hover:w-7"
)}
isDisabled={!isAllowed}
variant="plain"
size="md"
ariaLabel="add-comment"
>
<FontAwesomeIcon icon={faComment} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
</PopoverTrigger>
<PopoverContent
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
sticky="always"
>
<FormControl label="Comment" className="mb-0">
<TextArea
isDisabled={isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly}
className="border border-mineshaft-600 text-sm"
{...register(`secrets.${index}.comment`)}
rows={8}
cols={30}
/>
</FormControl>
</PopoverContent>
</Popover>
</div>
</div>
<div className="duration-0 flex w-16 justify-center overflow-hidden border-l border-mineshaft-600 pl-2 transition-all">
<div className="flex h-8 items-center space-x-2.5">
@@ -457,29 +491,29 @@ export const SecretInputRow = memo(
</Tooltip>
</div>
)}
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete">
<IconButton
size="lg"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
onClick={() => {
onSecretDelete(index, secKey, secId, idOverride);
}}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
<div className="opacity-0 group-hover:opacity-100">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
renderTooltip
allowedLabel="Delete"
>
{(isAllowed) => (
<IconButton
size="lg"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
onClick={() => {
onSecretDelete(index, secKey, secId, idOverride);
}}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
</div>
</div>
</td>