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