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
View File
@@ -19,6 +19,7 @@
"@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"argon2": "^0.30.3",
"aws-sdk": "^2.1364.0",
"axios": "^1.3.5",
+1
View File
@@ -10,6 +10,7 @@
"@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"argon2": "^0.30.3",
"aws-sdk": "^2.1364.0",
"axios": "^1.3.5",
@@ -1,7 +1,7 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { validateRequest } from "../../../helpers/validation";
import { Secret } from "../../../models";
import { Folder, Secret } from "../../../models";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@@ -11,6 +11,7 @@ import { BadRequestError } from "../../../utils/errors";
import * as reqValidator from "../../../validation";
import { SecretVersion } from "../../models";
import { EESecretService } from "../../services";
import { getFolderWithPathFromId } from "../../../services/FolderService";
/**
* Return secret versions for secret with id [secretId]
@@ -164,10 +165,6 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment })
);
// validate secret version
const oldSecretVersion = await SecretVersion.findOne({
@@ -194,6 +191,15 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
keyEncoding
} = oldSecretVersion;
let secretPath = "/";
const folders = await Folder.findOne({ workspace, environment });
if (folders)
secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/";
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath })
);
// update secret
const secret = await Secret.findByIdAndUpdate(
secretId,
+27 -5
View File
@@ -3,11 +3,31 @@ import {
ForcedSubject,
MongoAbility,
RawRuleOf,
buildMongoQueryMatcher,
createMongoAbility
} from "@casl/ability";
import { Membership } from "../models";
import { IRole } from "../models/role";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
const $glob: FieldInstruction<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const permissionSecretGlobPath = node.value;
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export enum ProjectPermissionActions {
Read = "read",
@@ -36,7 +56,7 @@ export enum ProjectPermissionSub {
type SubjectFields = {
environment: string;
secretPath?: string;
secretPath: string;
};
export type ProjectPermissionSet =
@@ -144,7 +164,7 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace);
return build();
return build({ conditionsMatcher });
};
export const adminProjectPermissions = buildAdminPermission();
@@ -180,7 +200,7 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build();
return build({ conditionsMatcher });
};
export const memberProjectPermissions = buildMemberPermission();
@@ -203,7 +223,7 @@ const buildViewerPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build();
return build({ conditionsMatcher });
};
export const viewerProjectPermission = buildViewerPermission();
@@ -228,7 +248,9 @@ export const getUserProjectPermissions = async (userId: string, workspaceId: str
if (membership.role === "viewer") return { permission: viewerProjectPermission, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions);
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
}
+6 -3
View File
@@ -3,6 +3,7 @@ import { MembershipOrg } from "../models";
import { IRole } from "../models/role";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
import { ACCEPTED } from "../variables";
import { conditionsMatcher } from "./ProjectRoleService";
export enum OrgPermissionActions {
Read = "read",
@@ -74,7 +75,7 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
return build();
return build({ conditionsMatcher });
};
export const adminPermissions = buildAdminPermission();
@@ -92,7 +93,7 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
return build();
return build({ conditionsMatcher });
};
export const memberPermissions = buildMemberPermission();
@@ -119,7 +120,9 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
if (membership.role === "member") return { permission: memberPermissions, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions);
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
}
+6 -2
View File
@@ -11,7 +11,9 @@ export const CreateRoleSchema = z.object({
.object({
subject: z.string().trim(),
action: z.string().trim(),
conditions: z.record(z.union([z.string().trim(), z.number()])).optional()
conditions: z
.record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })]))
.optional()
})
.array()
})
@@ -31,7 +33,9 @@ export const UpdateRoleSchema = z.object({
.object({
subject: z.string().trim(),
action: z.string().trim(),
conditions: z.record(z.union([z.string().trim(), z.number()])).optional()
conditions: z
.record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })]))
.optional()
})
.array()
.optional()
+2 -2
View File
@@ -189,7 +189,7 @@ export const BatchSecretsV2 = z.object({
workspaceId: z.string().trim(),
folderId: z.string().trim().default("root"),
environment: z.string().trim(),
secretPath: z.string().trim().optional(),
secretPath: z.string().trim().default("/"),
requests: z
.discriminatedUnion("method", [
z.object({
@@ -328,7 +328,7 @@ export const CreateSecretV3 = z.object({
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
metadata: z.record(z.string()).optional(),
metadata: z.record(z.string()).optional()
}),
params: z.object({
secretName: z.string().trim()
+15
View File
@@ -41,6 +41,7 @@
"@stripe/stripe-js": "^1.46.0",
"@tanstack/react-query": "^4.23.0",
"@types/argon2-browser": "^1.18.1",
"@ucast/mongo2js": "^1.3.4",
"add": "^2.0.6",
"argon2-browser": "^1.18.0",
"axios": "^0.27.2",
@@ -65,6 +66,7 @@
"markdown-it": "^13.0.1",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
"posthog-js": "^1.58.0",
"query-string": "^7.1.3",
"react": "^17.0.2",
@@ -106,6 +108,7 @@
"@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4",
"@types/node": "^18.11.9",
"@types/picomatch": "^2.3.0",
"@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
@@ -8391,6 +8394,12 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"node_modules/@types/picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==",
"dev": true
},
"node_modules/@types/pretty-hrtime": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz",
@@ -29414,6 +29423,12 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"@types/picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==",
"dev": true
},
"@types/pretty-hrtime": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz",
+3
View File
@@ -49,6 +49,7 @@
"@stripe/stripe-js": "^1.46.0",
"@tanstack/react-query": "^4.23.0",
"@types/argon2-browser": "^1.18.1",
"@ucast/mongo2js": "^1.3.4",
"add": "^2.0.6",
"argon2-browser": "^1.18.0",
"axios": "^0.27.2",
@@ -73,6 +74,7 @@
"markdown-it": "^13.0.1",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
"posthog-js": "^1.58.0",
"query-string": "^7.1.3",
"react": "^17.0.2",
@@ -114,6 +116,7 @@
"@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4",
"@types/node": "^18.11.9",
"@types/picomatch": "^2.3.0",
"@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
@@ -1,10 +1,11 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Checkbox, PopoverContent } from "@app/components/v2";
import { Button, Checkbox, PopoverContent } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { WsTag } from "../../hooks/api/tags/types";
import { ProjectPermissionCan } from "../permissions";
interface Props {
wsTags: WsTag[] | undefined;
@@ -14,7 +15,7 @@ interface Props {
handleTagOnMouseEnter: (wsTag: WsTag) => void;
handleTagOnMouseLeave: () => void;
checkIfTagIsVisible: (wsTag: WsTag) => boolean;
handleOnCreateTagOpen: () => void
handleOnCreateTagOpen: () => void;
}
const AddTagPopoverContent = ({
@@ -39,40 +40,53 @@ const AddTagPopoverContent = ({
<div className="absolute left-0 w-full border-mineshaft-600 border-t mt-2" />
<div className="flex flex-col space-y-1.5">
{wsTags?.map((wsTag: WsTag) => (
<div key={`tag-${wsTag._id}`} className="mt-4 h-[32px] relative flex items-center justify-start hover:border-mineshaft-600 hover:border hover:bg-mineshaft-700 p-2 rounded-md hover:text-bunker-200 bg-none"
<div
key={`tag-${wsTag._id}`}
className="mt-4 h-[32px] relative flex items-center justify-start hover:border-mineshaft-600 hover:border hover:bg-mineshaft-700 p-2 rounded-md hover:text-bunker-200 bg-none"
onClick={() => handleSelectTag(wsTag)}
onMouseEnter={() => handleTagOnMouseEnter(wsTag)}
onMouseLeave={() => handleTagOnMouseLeave()}
tabIndex={0} role="button"
onKeyDown={() => { }}>
{
(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && <Checkbox
tabIndex={0}
role="button"
onKeyDown={() => {}}
>
{(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && (
<Checkbox
id="autoCapitalization"
isChecked={selectedTagIds?.[wsTag.slug]}
className="absolute top-[50%] translate-y-[-50%] left-[10px] "
checkIndicatorBg={`${!selectedTagIds?.[wsTag.slug] ? "text-transparent" : "text-mineshaft-800"}`}
checkIndicatorBg={`${
!selectedTagIds?.[wsTag.slug] ? "text-transparent" : "text-mineshaft-800"
}`}
/>
}
)}
<div className="ml-7 flex items-center gap-3">
<div className="w-[10px] h-[10px] rounded-full" style={{ background: wsTag?.tagColor ? wsTag.tagColor : "#bec2c8" }}> </div>
<span >
{wsTag.slug}
</span>
<div
className="w-[10px] h-[10px] rounded-full"
style={{ background: wsTag?.tagColor ? wsTag.tagColor : "#bec2c8" }}
>
{" "}
</div>
<span>{wsTag.slug}</span>
</div>
</div>
))}
<div
className="h-[32px] relative flex items-center cursor-pointer justify-start border-mineshaft-600 border bg-mineshaft-700 p-2 rounded-md hover:text-bunker-200 bg-none"
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
{(isAllowed) => (
<Button
onClick={() => handleOnCreateTagOpen()}
tabIndex={0} role="button"
onKeyDown={() => { }}>
<FontAwesomeIcon icon={faPlus} className="ml-1 mr-2" />
<span> Add new tag</span>
</div>
isDisabled={!isAllowed}
size="xs"
className="mt-2"
leftIcon={<FontAwesomeIcon icon={faPlus} className="ml-1 mr-2" />}
>
Add new tag
</Button>
)}
</ProjectPermissionCan>
</div>
</PopoverContent>
)
}
);
};
export default AddTagPopoverContent
export default AddTagPopoverContent;
@@ -0,0 +1,40 @@
import { ReactNode } from "react";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
type Props = {
containerClassName?: string;
className?: string;
children?: ReactNode;
};
export const PermissionDeniedBanner = ({ containerClassName, className, children }: Props) => {
return (
<div
className={twMerge(
"container h-full mx-auto flex justify-center items-center",
containerClassName
)}
>
<div
className={twMerge(
"rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="text-4xl font-medium mb-2">Permission Denied</div>
{children || (
<div className="text-sm">
You do not have permission. <br /> Kindly contact your organization administrator
</div>
)}
</div>
</div>
</div>
);
};
@@ -11,7 +11,11 @@ type Props = {
// so when permission is allowed same tooltip will be reused to show helpertext
renderTooltip?: boolean;
allowedLabel?: string;
} & BoundCanProps<TProjectPermission>;
// BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission
// For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error
// tried a lot combinations
// REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15
} & BoundCanProps<any>;
export const ProjectPermissionCan: FunctionComponent<Props> = ({
label = "Permission Denied. Kindly contact your project admin",
@@ -22,7 +26,6 @@ export const ProjectPermissionCan: FunctionComponent<Props> = ({
...props
}) => {
const permission = useProjectPermission();
return (
<Can {...props} passThrough={passThrough} ability={props?.ability || permission}>
{(isAllowed, ability) => {
@@ -1,2 +1,3 @@
export { OrgPermissionCan } from "./OrgPermissionCan";
export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
export { ProjectPermissionCan } from "./ProjectPermissionCan";
@@ -27,7 +27,7 @@ export enum ProjectPermissionSub {
type SubjectFields = {
environment: string;
secretPath?: string;
secretPath: string;
};
export type ProjectPermissionSet =
+27 -2
View File
@@ -1,6 +1,8 @@
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { buildMongoQueryMatcher, createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
import { useQuery } from "@tanstack/react-query";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
import { apiRequest } from "@app/config/request";
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
@@ -13,6 +15,29 @@ import {
TRole
} from "./types";
const $glob: FieldInstruction<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const permissionSecretGlobPath = node.value;
if (!secretPath) return false;
// console.log(
// secretPath,
// picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }),
// permissionSecretGlobPath
// );
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export const roleQueryKeys = {
getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const,
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
@@ -57,7 +82,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
enabled: Boolean(orgId),
select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(data);
const ability = createMongoAbility<OrgPermissionSet>(rule);
const ability = createMongoAbility<OrgPermissionSet>(rule, { conditionsMatcher });
return ability;
}
});
+11 -1
View File
@@ -739,16 +739,26 @@ export const AppLayout = ({ children }: LayoutProps) => {
<Controller
control={control}
name="addMembers"
defaultValue
defaultValue={false}
render={({ field: { onBlur, value, onChange } }) => (
<OrgPermissionCan
I={OrgPermissionActions.Read}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<div>
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
isDisabled={!isAllowed}
onBlur={onBlur}
>
Add all members of my organization to this project
</Checkbox>
</div>
)}
</OrgPermissionCan>
)}
/>
</div>
+11 -1
View File
@@ -838,16 +838,26 @@ const OrganizationPage = withPermission(
<Controller
control={control}
name="addMembers"
defaultValue
defaultValue={false}
render={({ field: { onBlur, value, onChange } }) => (
<OrgPermissionCan
I={OrgPermissionActions.Read}
a={OrgPermissionSubjects.Member}
>
{(isAllowed) => (
<div>
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
isDisabled={!isAllowed}
onBlur={onBlur}
>
Add all members of my organization to this project
</Checkbox>
</div>
)}
</OrgPermissionCan>
)}
/>
</div>
@@ -3,6 +3,7 @@ import { FormProvider, useFieldArray, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { subject } from "@casl/ability";
import { Can } from "@casl/react";
import {
closestCenter,
DndContext,
@@ -26,7 +27,6 @@ import {
faEyeSlash,
faFileImport,
faFolderPlus,
faLock,
faMagnifyingGlass,
faPlus
} from "@fortawesome/free-solid-svg-icons";
@@ -41,7 +41,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
@@ -66,7 +66,6 @@ import {
useSubscription,
useWorkspace
} from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
import {
useBatchSecretsOp,
@@ -92,7 +91,6 @@ import {
useUpdateSecretImport
} from "@app/hooks/api";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { WorkspaceEnv } from "@app/hooks/api/types";
import { CompareSecret } from "./components/CompareSecret";
import { CreateTagModal } from "./components/CreateTagModal";
@@ -133,14 +131,13 @@ type TDeleteSecretImport = { environment: string; secretPath: string };
* Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving
* They will get it back
*/
export const DashboardPage = withProjectPermission(
() => {
export const DashboardPage = () => {
const { subscription } = useSubscription();
const { t } = useTranslation();
const router = useRouter();
const { createNotification } = useNotificationContext();
const queryClient = useQueryClient();
const envQuery = router.query.env as string;
const environment = router.query.env as string;
const permission = useProjectPermission();
const secretContainer = useRef<HTMLDivElement | null>(null);
@@ -159,7 +156,6 @@ export const DashboardPage = withProjectPermission(
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
const [searchFilter, setSearchFilter] = useState("");
const [snapshotId, setSnaphotId] = useState<string | null>(null);
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
const deletedSecretIds = useRef<{ id: string; secretName: string }[]>([]);
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
@@ -170,7 +166,6 @@ export const DashboardPage = withProjectPermission(
const { currentWorkspace, isLoading } = useWorkspace();
const { currentOrg } = useOrganization();
const workspaceId = currentWorkspace?._id as string;
const selectedEnvSlug = selectedEnv?.slug || "";
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
@@ -193,7 +188,7 @@ export const DashboardPage = withProjectPermission(
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
env: selectedEnvSlug,
env: environment,
decryptFileKey: latestFileKey!,
isPaused: Boolean(snapshotId),
folderId
@@ -201,7 +196,7 @@ export const DashboardPage = withProjectPermission(
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
workspaceId: workspaceId || "",
environment: selectedEnvSlug,
environment,
parentFolderId: folderId,
isPaused: isRollbackMode,
sortDir
@@ -214,7 +209,7 @@ export const DashboardPage = withProjectPermission(
isFetchingNextPage
} = useGetWorkspaceSecretSnapshots({
workspaceId,
environment: selectedEnvSlug,
environment,
folder: folderId,
limit: 10
});
@@ -225,13 +220,13 @@ export const DashboardPage = withProjectPermission(
isFetching: isSnapshotChanging
} = useGetSnapshotSecrets({
snapshotId: snapshotId || "",
env: selectedEnvSlug,
env: environment,
decryptFileKey: latestFileKey!
});
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
workspaceId,
selectedEnvSlug,
environment,
folderId
);
@@ -248,35 +243,38 @@ export const DashboardPage = withProjectPermission(
const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports(
workspaceId,
selectedEnvSlug,
environment,
folderId
);
const { data: importedSecrets } = useGetImportedSecrets({
workspaceId,
decryptFileKey: latestFileKey!,
environment: selectedEnvSlug,
environment,
folderId
});
const secretPath = `/${(folderData?.dir || [])
?.filter(({ name }) => name !== "root")
?.map(({ name }) => name)
.join("/")}`;
const userAvailableEnvs = currentWorkspace?.environments?.filter(({ slug }) =>
const userAvailableEnvs = currentWorkspace?.environments?.filter(
({ slug }) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath })
) ||
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Folders, { environment: slug, secretPath })
) ||
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment: slug, secretPath })
)
);
useEffect(() => {
if (!isLoading && currentWorkspace) {
const env = userAvailableEnvs?.find(({ slug }) => slug === envQuery);
if (env) setSelectedEnv(env);
}
}, [isLoading, workspaceId, userAvailableEnvs]);
// This is for dnd-kit. As react-query state mutation async
// This will act as a placeholder to avoid a glitching animation on dropping items
const [items, setItems] = useState<
@@ -327,14 +325,15 @@ export const DashboardPage = withProjectPermission(
} = method;
const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" });
const isReadOnly =
permission.can(
const isReadOnly = isFoldersLoading
? true
: permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: selectedEnvSlug })
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
) &&
permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: selectedEnvSlug })
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const canDoRollback = !isReadOnly;
@@ -424,12 +423,12 @@ export const DashboardPage = withProjectPermission(
await performSecretRollback({
workspaceId,
version: snapshotSecret.version,
environment: selectedEnvSlug,
environment,
folderId
});
setValue("isSnapshotMode", false);
setSnaphotId(null);
queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug));
queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, environment));
createNotification({
text: "Successfully rollback secrets",
type: "success"
@@ -464,7 +463,6 @@ export const DashboardPage = withProjectPermission(
secrets?.secrets
);
// type check
if (!selectedEnv?.slug) return;
if (batchedSecret.length === 0) {
reset();
return;
@@ -474,7 +472,7 @@ export const DashboardPage = withProjectPermission(
requests: batchedSecret,
workspaceId,
folderId,
environment: selectedEnv?.slug
environment
});
createNotification({
text: "Successfully saved changes",
@@ -503,8 +501,6 @@ export const DashboardPage = withProjectPermission(
if (!window.confirm(leaveConfirmDefaultMessage)) return;
}
const env = userAvailableEnvs?.find((el) => el.slug === slug);
if (env) setSelectedEnv(env);
const query: Record<string, string> = { ...router.query, env: slug };
delete query.folderId;
router.push({
@@ -520,7 +516,7 @@ export const DashboardPage = withProjectPermission(
secretsFromImport.push({ key: el.key, value: el.value, comment: el.comment });
});
});
downloadSecret(getValues("secrets"), secretsFromImport, selectedEnv?.slug);
downloadSecret(getValues("secrets"), secretsFromImport, environment);
};
// record all deleted ids
@@ -576,12 +572,12 @@ export const DashboardPage = withProjectPermission(
pathname: router.pathname,
query: {
id: workspaceId,
env: envQuery,
env: environment,
folderId: id
}
});
},
[envQuery, workspaceId]
[environment, workspaceId]
);
const isEditFolder = Boolean(popUp?.folderForm?.data);
@@ -591,7 +587,7 @@ export const DashboardPage = withProjectPermission(
try {
await createFolder({
workspaceId,
environment: selectedEnv?.slug || "",
environment,
folderName: name,
parentFolderId: folderId
});
@@ -616,7 +612,7 @@ export const DashboardPage = withProjectPermission(
await updateFolder({
folderId: id,
workspaceId,
environment: selectedEnv?.slug || "",
environment,
name
});
createNotification({
@@ -632,7 +628,7 @@ export const DashboardPage = withProjectPermission(
});
}
},
[selectedEnv?.slug, (popUp?.folderForm?.data as TDeleteFolderForm)?.id]
[environment, (popUp?.folderForm?.data as TDeleteFolderForm)?.id]
);
const handleFolderDelete = useCallback(async () => {
@@ -640,7 +636,7 @@ export const DashboardPage = withProjectPermission(
try {
deleteFolder({
workspaceId,
environment: selectedEnv?.slug || "",
environment,
folderId: id
});
createNotification({
@@ -655,14 +651,14 @@ export const DashboardPage = withProjectPermission(
type: "error"
});
}
}, [selectedEnv?.slug, (popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]);
}, [(popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]);
// SECRET IMPORT SECTION
const handleSecretImportCreate = async (env: string, secPath: string) => {
try {
await createSecretImport({
workspaceId,
environment: selectedEnv?.slug || "",
environment,
folderId,
secretImport: {
environment: env,
@@ -690,7 +686,7 @@ export const DashboardPage = withProjectPermission(
if (secretImportCfg?._id) {
await deleteSecretImport({
workspaceId,
environment: selectedEnvSlug,
environment,
folderId,
id: secretImportCfg?._id,
secretImportEnv: importEnv,
@@ -720,7 +716,7 @@ export const DashboardPage = withProjectPermission(
setItems(newImportOrder);
updateSecretImportSync({
workspaceId,
environment: selectedEnvSlug,
environment,
folderId,
id: secretImportCfg?._id || "",
secretImports: newImportOrder.map((el) => ({
@@ -773,30 +769,6 @@ export const DashboardPage = withProjectPermission(
);
}
if (
permission.cannot(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: envQuery, secretPath })
)
) {
return (
<div className="container h-full mx-auto flex justify-center items-center">
<div className="rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end">
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="text-4xl font-medium mb-2">Permission Denied</div>
<div className="text-sm">
You do not have permission to this page. <br /> Kindly contact your organization
administrator
</div>
</div>
</div>
</div>
);
}
return (
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<form autoComplete="off" className="h-full flex flex-col">
@@ -805,7 +777,7 @@ export const DashboardPage = withProjectPermission(
<NavHeader
pageName={t("dashboard.title")}
currentEnv={
userAvailableEnvs?.filter((envir) => envir.slug === envQuery)?.[0]?.name || ""
userAvailableEnvs?.filter((envir) => envir.slug === environment)?.[0]?.name || ""
}
isFolderMode
folders={folderData?.dir}
@@ -918,7 +890,10 @@ export const DashboardPage = withProjectPermission(
<div className="flex flex-row items-center justify-center">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, {
secretPath,
environment
})}
>
{(isAllowed) => (
<button
@@ -952,9 +927,10 @@ export const DashboardPage = withProjectPermission(
<DropdownMenuContent align="end" className="left-20 z-[60] mt-1 w-[10.8rem]">
<div className="rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<div className="w-full pb-1">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Folders}
<Can
ability={permission}
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SecretRollback}
>
{(isAllowed) => (
<Button
@@ -968,12 +944,15 @@ export const DashboardPage = withProjectPermission(
Add Folder
</Button>
)}
</ProjectPermissionCan>
</Can>
</div>
<div className="w-full">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SecretImports}
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.SecretImports, {
environment,
secretPath
})}
>
{(isAllowed) => (
<Button
@@ -1016,16 +995,17 @@ export const DashboardPage = withProjectPermission(
a={
isRollbackMode
? ProjectPermissionSub.SecretRollback
: ProjectPermissionSub.Secrets
: subject(ProjectPermissionSub.Secrets, {
environment,
secretPath
})
}
>
{(isAllowed) => (
<Button
isDisabled={isSubmitDisabled || !isAllowed}
isLoading={isSubmitting}
leftIcon={
<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />
}
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
onClick={handleSubmit(onSaveSecret)}
className="h-10 text-black"
color="primary"
@@ -1055,6 +1035,8 @@ export const DashboardPage = withProjectPermission(
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
<tbody className="max-h-96 overflow-y-auto">
<SecretImportSection
environment={environment}
secretPath={secretPath}
onSecretImportDelete={handleSecretImportDelPopUpOpen}
secrets={secrets?.secrets}
importedSecrets={importedSecrets}
@@ -1062,15 +1044,23 @@ export const DashboardPage = withProjectPermission(
searchTerm={searchFilter}
/>
<FolderSection
environment={environment}
secretPath={secretPath}
onFolderOpen={handleFolderOpen}
onFolderUpdate={handleFolderCreatePopUpOpen}
onFolderDelete={handleFolderDeletePopUpOpen}
folders={folderList}
search={searchFilter}
/>
{fields.map(({ id, _id }, index) => (
{permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
) ? (
fields.map(({ id, _id }, index) => (
<SecretInputRow
key={id}
environment={environment}
secretPath={secretPath}
secUniqId={_id}
isReadOnly={isReadOnly}
isRollbackMode={isRollbackMode}
@@ -1088,13 +1078,23 @@ export const DashboardPage = withProjectPermission(
setValue={setValue}
autoCapitalization={currentWorkspace?.autoCapitalization}
/>
))}
))
) : (
<tr>
<td colSpan={3}>
<PermissionDeniedBanner />
</td>
</tr>
)}
{!isReadOnly && !isRollbackMode && (
<tr>
<td colSpan={3} className="hover:bg-mineshaft-700">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath
})}
>
{(isAllowed) => (
<button
@@ -1128,6 +1128,8 @@ export const DashboardPage = withProjectPermission(
onSelectSnapshot={setSnaphotId}
/>
<SecretDetailDrawer
environment={environment}
secretPath={secretPath}
onSave={handleSubmit(onSaveSecret)}
isReadOnly={isReadOnly || isRollbackMode}
onSecretDelete={onSecretDelete}
@@ -1139,6 +1141,8 @@ export const DashboardPage = withProjectPermission(
/>
</FormProvider>
<SecretDropzone
environment={environment}
secretPath={secretPath}
workspaceId={workspaceId}
isSmaller={!isEmptyPage}
onParsedEnv={handleUploadedEnv}
@@ -1267,6 +1271,4 @@ export const DashboardPage = withProjectPermission(
)}
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Secrets }
);
};
@@ -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>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button isDisabled={!isAllowed} variant="star" size={isSmaller ? "xs" : "sm"}>
<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,
@@ -16,6 +17,8 @@ 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">
<ProjectPermissionCan
renderTooltip
allowedLabel="Add Tags"
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<IconButton
isDisabled={!isAllowed}
variant="plain"
size="md"
ariaLabel="add-tag"
ariaLabel="add-tags"
className="py-[0.42rem]"
>
<FontAwesomeIcon icon={faTags} />
</IconButton>
</Tooltip>
)}
</ProjectPermissionCan>
</div>
</PopoverTrigger>
<AddTagPopoverContent
@@ -390,7 +403,13 @@ export const SecretInputRow = memo(
<div className="flex h-8 flex-row items-center pr-2">
{!isAddOnly && (
<div>
<Tooltip content="Override with a personal value">
<ProjectPermissionCan
renderTooltip
allowedLabel="Override with a personal value"
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<IconButton
variant="plain"
className={twMerge(
@@ -399,46 +418,61 @@ export const SecretInputRow = memo(
)}
onClick={onSecretOverride}
size="md"
isDisabled={isRollbackMode || isReadOnly}
isDisabled={isRollbackMode || isReadOnly || !isAllowed}
ariaLabel="info"
>
<div className="flex items-center space-x-1">
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
</div>
</IconButton>
</Tooltip>
)}
</ProjectPermissionCan>
</div>
)}
<Tooltip content="Comment">
<div className="mt-0.5 overflow-hidden ">
<Popover>
<PopoverTrigger asChild>
<div>
<ProjectPermissionCan
renderTooltip
allowedLabel="Comment"
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(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-tag"
ariaLabel="add-comment"
>
<FontAwesomeIcon icon={faComment} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
</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()}
<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>
</Tooltip>
</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,13 +491,14 @@ export const SecretInputRow = memo(
</Tooltip>
</div>
)}
<div className="opacity-0 group-hover:opacity-100">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
renderTooltip
allowedLabel="Delete"
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete">
<IconButton
size="lg"
variant="plain"
@@ -476,12 +511,11 @@ export const SecretInputRow = memo(
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</div>
</div>
</div>
</td>
</tr>
);
@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
import {
Checkbox,
FormControl,
Input,
Select,
SelectItem,
@@ -68,7 +69,7 @@ export const MultiEnvProjectPermission = ({
const handlePermissionChange = (val: Permission) => {
switch (val) {
case Permission.NoAccess:
setValue(`permissions.${formName}`, {}, { shouldDirty: true });
setValue(`permissions.${formName}`, undefined, { shouldDirty: true });
break;
case Permission.FullAccess:
setValue(
@@ -151,7 +152,14 @@ export const MultiEnvProjectPermission = ({
name={`permissions.${formName}.${slug}.secretPath`}
control={control}
render={({ field }) => (
<Input {...field} className="w-full overflow-ellipsis" />
/* eslint-disable-next-line no-template-curly-in-string */
<FormControl helperText="Ex pattern: /, /**, /{folder1,folder2}">
<Input
{...field}
className="w-full overflow-ellipsis"
placeholder="Glob patterns are supported"
/>
</FormControl>
)}
/>
</Td>
@@ -133,6 +133,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
const handleRoleUpdate = async (el: TFormSchema) => {
if (!role?._id) return;
console.log(el);
try {
await updateRole({
orgId,
@@ -73,7 +73,7 @@ const multiEnvApi2Form = (
}
const secretEnv = permission?.conditions?.environment || "all";
const secretPath = permission?.conditions?.secretPath;
const secretPath = permission?.conditions?.secretPath?.$glob;
// initialize
if (formVal && !formVal?.[secretEnv]) {
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
@@ -109,6 +109,8 @@ const multiEnvForm2Api = (
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
subject: (typeof MULTI_ENV_KEY)[number]
) => {
if (!formVal) return;
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
// if any of them is set in all push it without any condition
PERMISSION_ACTIONS.forEach((action) => {
@@ -130,7 +132,8 @@ const multiEnvForm2Api = (
// if not full access for an action
if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) {
const conditions: Record<string, unknown> = { environment: slug };
if (formVal[slug]?.secretPath) conditions.secretPath = formVal?.[slug]?.secretPath;
if (formVal[slug]?.secretPath)
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
permissions.push({ action, subject, conditions });
}
@@ -141,17 +144,22 @@ 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);
});
// 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(([rule, actions]) => {
if (MULTI_ENV_KEY.includes(rule as (typeof MULTI_ENV_KEY)[number])) {
multiEnvForm2Api(
permissions,
JSON.parse(JSON.stringify(actions || {})),
rule as (typeof MULTI_ENV_KEY)[number]
);
} else {
Object.entries(actions).forEach(([action, isAllowed]) => {
if (isAllowed) {
permissions.push({ subject: rule, action });
}
});
}
});
return permissions;
};
@@ -13,6 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { PermissionDeniedBanner } from "@app/components/permissions";
import {
Button,
EmptyState,
@@ -36,7 +37,6 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { withProjectPermission } from "@app/hoc";
import {
useCreateSecretV3,
useDeleteSecretV3,
@@ -50,7 +50,7 @@ import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
const SecretOverview = () => {
export const SecretOverviewPage = () => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
@@ -91,10 +91,19 @@ const SecretOverview = () => {
}, [isWorkspaceLoading, workspaceId, router.isReady]);
const userAvailableEnvs =
currentWorkspace?.environments?.filter(({ slug }) =>
currentWorkspace?.environments?.filter(
({ slug }) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath: secretPath || "/" })
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath })
) ||
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Folders, { environment: slug, secretPath })
) ||
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment: slug, secretPath })
)
) || [];
@@ -371,8 +380,10 @@ const SecretOverview = () => {
onClick={handleFolderClick}
/>
))}
{filteredSecretNames.map((key, index) => (
{userAvailableEnvs?.length > 0 ? (
filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
secretPath={secretPath}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
@@ -382,7 +393,10 @@ const SecretOverview = () => {
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
/>
))}
))
) : (
<PermissionDeniedBanner />
)}
</TBody>
<TFoot>
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
@@ -414,8 +428,3 @@ const SecretOverview = () => {
</div>
);
};
export const SecretOverviewPage = withProjectPermission(SecretOverview, {
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Secrets
});
@@ -1,4 +1,5 @@
import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -14,6 +15,7 @@ type Props = {
isCreatable?: boolean;
isVisible?: boolean;
environment: string;
secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretDelete: (env: string, key: string) => Promise<void>;
@@ -27,6 +29,7 @@ export const SecretEditRow = ({
onSecretCreate,
onSecretDelete,
environment,
secretPath,
isVisible
}: Props) => {
const {
@@ -95,7 +98,7 @@ export const SecretEditRow = ({
<>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<div>
@@ -18,6 +18,7 @@ import { SecretEditRow } from "./SecretEditRow";
type Props = {
secretKey: string;
secretPath: string;
environments: { name: string; slug: string }[];
expandableColWidth: number;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
@@ -29,6 +30,7 @@ type Props = {
export const SecretOverviewTableRow = ({
secretKey,
environments = [],
secretPath,
getSecretByKey,
onSecretUpdate,
onSecretCreate,
@@ -73,9 +75,11 @@ export const SecretOverviewTableRow = ({
>
<div className="h-full w-full border-r border-mineshaft-600 py-[0.85rem] px-5">
<div className="flex justify-center">
{!isSecretEmpty && <Tooltip content={isSecretPresent ? "Present secret" : "Missing secret"}>
{!isSecretEmpty && (
<Tooltip content={isSecretPresent ? "Present secret" : "Missing secret"}>
<FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />
</Tooltip>}
</Tooltip>
)}
{isSecretEmpty && (
<Tooltip content="Empty value">
<FontAwesomeIcon icon={faCircle} />
@@ -141,6 +145,7 @@ export const SecretOverviewTableRow = ({
</td>
<td className="col-span-2 h-8 w-full">
<SecretEditRow
secretPath={secretPath}
isVisible={isSecretVisible}
secretName={secretKey}
defaultValue={secret?.value}
@@ -2,32 +2,26 @@ import { useTranslation } from "react-i18next";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { OrgPermissionCan, PermissionDeniedBanner } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { usePopUp } from "@app/hooks";
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
export const OrgIncidentContactsSection = withPermission(
() => {
export const OrgIncidentContactsSection = () => {
const { t } = useTranslation();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact"
] as const);
const permission = useOrgPermission();
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-4">
<p className="min-w-max text-xl font-semibold">
{t("section.incident.incident-contacts")}
</p>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.IncidentAccount}
>
<p className="min-w-max text-xl font-semibold">{t("section.incident.incident-contacts")}</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}>
{(isAllowed) => (
<Button
colorSchema="secondary"
@@ -41,7 +35,11 @@ export const OrgIncidentContactsSection = withPermission(
)}
</OrgPermissionCan>
</div>
{permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount) ? (
<OrgIncidentContactsTable />
) : (
<PermissionDeniedBanner />
)}
<AddOrgIncidentContactModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -49,6 +47,4 @@ export const OrgIncidentContactsSection = withPermission(
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.IncidentAccount }
);
};
@@ -7,7 +7,6 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { useRenameOrg } from "@app/hooks/api";
const formSchema = yup.object({
@@ -16,8 +15,7 @@ const formSchema = yup.object({
type FormData = yup.InferType<typeof formSchema>;
export const OrgNameChangeSection = withPermission(
(): JSX.Element => {
export const OrgNameChangeSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { handleSubmit, control, reset } = useForm<FormData>({
@@ -83,10 +81,4 @@ export const OrgNameChangeSection = withPermission(
</OrgPermissionCan>
</form>
);
},
{
action: OrgPermissionActions.Read,
subject: OrgPermissionSubjects.Settings,
containerClassName: "mb-4"
}
);
};
@@ -3,8 +3,6 @@ import { useTranslation } from "react-i18next";
import { Tab } from "@headlessui/react";
import NavHeader from "@app/components/navigation/NavHeader";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
@@ -16,8 +14,7 @@ const tabs = [
{ name: "Webhooks", key: "tab-project-webhooks" }
];
export const ProjectSettingsPage = withProjectPermission(
() => {
export const ProjectSettingsPage = () => {
const { t } = useTranslation();
return (
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
@@ -60,6 +57,4 @@ export const ProjectSettingsPage = withProjectPermission(
</div>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
);
};
@@ -4,11 +4,9 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { ProjectPermissionCan } from "@app/components/permissions";
import { Checkbox } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useToggleAutoCapitalization } from "@app/hooks/api";
export const AutoCapitalizationSection = withProjectPermission(
() => {
export const AutoCapitalizationSection = () => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
@@ -59,6 +57,4 @@ export const AutoCapitalizationSection = withProjectPermission(
</ProjectPermissionCan>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
);
};
@@ -5,11 +5,9 @@ import {
} from "@app/components/utilities/cryptography/crypto";
import { Checkbox } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
export const E2EESection = withProjectPermission(
() => {
export const E2EESection = () => {
const { currentWorkspace } = useWorkspace();
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
@@ -109,9 +107,4 @@ export const E2EESection = withProjectPermission(
) : (
<div />
);
},
{
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Settings
}
);
};
@@ -2,15 +2,15 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useDeleteWsEnvironment } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -18,11 +18,11 @@ import { AddEnvironmentModal } from "./AddEnvironmentModal";
import { EnvironmentTable } from "./EnvironmentTable";
import { UpdateEnvironmentModal } from "./UpdateEnvironmentModal";
export const EnvironmentSection = withProjectPermission(
() => {
export const EnvironmentSection = () => {
const { createNotification } = useNotificationContext();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const permision = useProjectPermission();
const deleteWsEnvironment = useDeleteWsEnvironment();
@@ -94,7 +94,11 @@ export const EnvironmentSection = withProjectPermission(
Choose which environments will show up in your dashboard like development, staging,
production
</p>
{permision.can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments) ? (
<EnvironmentTable handlePopUpOpen={handlePopUpOpen} />
) : (
<PermissionDeniedBanner />
)}
<AddEnvironmentModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -123,6 +127,4 @@ export const EnvironmentSection = withProjectPermission(
/>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Environments }
);
};
@@ -5,7 +5,6 @@ import {
} from "@app/components/utilities/cryptography/crypto";
import { Button } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import {
useGetUserWsKey,
useGetWorkspaceIndexStatus,
@@ -16,8 +15,7 @@ import {
// TODO: add check so that this only shows up if user is
// an admin in the workspace
export const ProjectIndexSecretsSection = withProjectPermission(
() => {
export const ProjectIndexSecretsSection = () => {
const { currentWorkspace } = useWorkspace();
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
currentWorkspace?._id ?? ""
@@ -62,9 +60,9 @@ export const ProjectIndexSecretsSection = withProjectPermission(
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
<p className="text-gray-400 mb-8">
Your project, created before the introduction of blind indexing, contains unindexed
secrets. To access individual secrets by name through the SDK and public API, please
enable blind indexing.
Your project, created before the introduction of blind indexing, contains unindexed secrets.
To access individual secrets by name through the SDK and public API, please enable blind
indexing.
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
@@ -83,6 +81,4 @@ export const ProjectIndexSecretsSection = withProjectPermission(
) : (
<div />
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
);
};
@@ -2,10 +2,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteWsTag } from "@app/hooks/api";
@@ -14,13 +13,13 @@ import { SecretTagsTable } from "./SecretTagsTable";
type DeleteModalData = { name: string; id: string };
export const SecretTagsSection = withProjectPermission(
(): JSX.Element => {
export const SecretTagsSection = (): JSX.Element => {
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"CreateSecretTag",
"deleteTagConfirmation"
] as const);
const permission = useProjectPermission();
const deleteWsTag = useDeleteWsTag();
@@ -70,7 +69,11 @@ export const SecretTagsSection = withProjectPermission(
Every secret can be assigned to one or more tags. Here you can add and remove tags for the
current project.
</p>
{permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? (
<SecretTagsTable handlePopUpOpen={handlePopUpOpen} />
) : (
<PermissionDeniedBanner />
)}
<AddSecretTagModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -88,6 +91,4 @@ export const SecretTagsSection = withProjectPermission(
/>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Tags }
);
};