From a4ef829046ddbae217b4991d10a57499f858d0da Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Thu, 7 Sep 2023 15:20:05 +0530 Subject: [PATCH] feat(rbac): added glob support in permission and revealed settings --- backend/package-lock.json | 1 + backend/package.json | 1 + .../src/ee/controllers/v1/secretController.ts | 16 +- backend/src/services/ProjectRoleService.ts | 32 +- backend/src/services/RoleService.ts | 9 +- backend/src/validation/role.ts | 8 +- backend/src/validation/secrets.ts | 4 +- frontend/package-lock.json | 15 + frontend/package.json | 3 + .../AddTagPopoverContent.tsx | 144 +- .../permissions/PermissionDeniedBanner.tsx | 40 + .../permissions/ProjectPermissionCan.tsx | 7 +- frontend/src/components/permissions/index.tsx | 1 + .../context/ProjectPermissionContext/types.ts | 2 +- frontend/src/hooks/api/roles/queries.tsx | 29 +- frontend/src/layouts/AppLayout/AppLayout.tsx | 26 +- .../src/pages/org/[id]/overview/index.tsx | 26 +- .../src/views/DashboardPage/DashboardPage.tsx | 2046 +++++++++-------- .../FolderSection/FolderSection.tsx | 11 +- .../SecretDetailDrawer/SecretDetailDrawer.tsx | 13 +- .../SecretDropzone/SecretDropzone.tsx | 47 +- .../SecretImportSection/SecretImportItem.tsx | 11 +- .../SecretImportSection.tsx | 14 +- .../SecretInputRow/SecretInputRow.tsx | 206 +- .../MultiEnvProjectPermission.tsx | 12 +- .../ProjectRoleModifySection.tsx | 1 + .../ProjectRoleModifySection.utils.ts | 28 +- .../SecretOverviewPage/SecretOverviewPage.tsx | 57 +- .../SecretOverviewTableRow/SecretEditRow.tsx | 5 +- .../SecretOverviewTableRow.tsx | 11 +- .../OrgIncidentContactsSection.tsx | 80 +- .../OrgNameChangeSection.tsx | 132 +- .../ProjectSettingsPage.tsx | 89 +- .../AutoCapitalizationSection.tsx | 100 +- .../components/E2EESection/E2EESection.tsx | 195 +- .../EnvironmentSection/EnvironmentSection.tsx | 202 +- .../ProjectIndexSecretsSection.tsx | 126 +- .../SecretTagsSection/SecretTagsSection.tsx | 149 +- 38 files changed, 2068 insertions(+), 1831 deletions(-) create mode 100644 frontend/src/components/permissions/PermissionDeniedBanner.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index 3a65f61d6..f5c55cf56 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ "@sentry/tracing": "^7.48.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "@ucast/mongo2js": "^1.3.4", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", diff --git a/backend/package.json b/backend/package.json index 21f7af45a..08db10ecc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,7 @@ "@sentry/tracing": "^7.48.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", + "@ucast/mongo2js": "^1.3.4", "argon2": "^0.30.3", "aws-sdk": "^2.1364.0", "axios": "^1.3.5", diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index 1203f406d..58b7e6264 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -1,7 +1,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from "express"; import { validateRequest } from "../../../helpers/validation"; -import { Secret } from "../../../models"; +import { Folder, Secret } from "../../../models"; import { ProjectPermissionActions, ProjectPermissionSub, @@ -11,6 +11,7 @@ import { BadRequestError } from "../../../utils/errors"; import * as reqValidator from "../../../validation"; import { SecretVersion } from "../../models"; import { EESecretService } from "../../services"; +import { getFolderWithPathFromId } from "../../../services/FolderService"; /** * Return secret versions for secret with id [secretId] @@ -164,10 +165,6 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment }) - ); // validate secret version const oldSecretVersion = await SecretVersion.findOne({ @@ -194,6 +191,15 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { keyEncoding } = oldSecretVersion; + let secretPath = "/"; + const folders = await Folder.findOne({ workspace, environment }); + if (folders) + secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/"; + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath }) + ); + // update secret const secret = await Secret.findByIdAndUpdate( secretId, diff --git a/backend/src/services/ProjectRoleService.ts b/backend/src/services/ProjectRoleService.ts index 5a2d24e00..10dbd2302 100644 --- a/backend/src/services/ProjectRoleService.ts +++ b/backend/src/services/ProjectRoleService.ts @@ -3,11 +3,31 @@ import { ForcedSubject, MongoAbility, RawRuleOf, + buildMongoQueryMatcher, createMongoAbility } from "@casl/ability"; import { Membership } from "../models"; import { IRole } from "../models/role"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; +import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; +import picomatch from "picomatch"; + +const $glob: FieldInstruction = { + type: "field", + validate(instruction, value) { + if (typeof value !== "string") { + throw new Error(`"${instruction.name}" expects value to be a string`); + } + } +}; + +const glob: JsInterpreter> = (node, object, context) => { + const secretPath = context.get(object, node.field); + const permissionSecretGlobPath = node.value; + return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }); +}; + +export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); export enum ProjectPermissionActions { Read = "read", @@ -36,7 +56,7 @@ export enum ProjectPermissionSub { type SubjectFields = { environment: string; - secretPath?: string; + secretPath: string; }; export type ProjectPermissionSet = @@ -144,7 +164,7 @@ const buildAdminPermission = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace); - return build(); + return build({ conditionsMatcher }); }; export const adminProjectPermissions = buildAdminPermission(); @@ -180,7 +200,7 @@ const buildMemberPermission = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); - return build(); + return build({ conditionsMatcher }); }; export const memberProjectPermissions = buildMemberPermission(); @@ -203,7 +223,7 @@ const buildViewerPermission = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs); can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList); - return build(); + return build({ conditionsMatcher }); }; export const viewerProjectPermission = buildViewerPermission(); @@ -228,7 +248,9 @@ export const getUserProjectPermissions = async (userId: string, workspaceId: str if (membership.role === "viewer") return { permission: viewerProjectPermission, membership }; if (membership.role === "custom") { - const permission = createMongoAbility(membership.customRole.permissions); + const permission = createMongoAbility(membership.customRole.permissions, { + conditionsMatcher + }); return { permission, membership }; } diff --git a/backend/src/services/RoleService.ts b/backend/src/services/RoleService.ts index 648128246..bc5ef7453 100644 --- a/backend/src/services/RoleService.ts +++ b/backend/src/services/RoleService.ts @@ -3,6 +3,7 @@ import { MembershipOrg } from "../models"; import { IRole } from "../models/role"; import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; import { ACCEPTED } from "../variables"; +import { conditionsMatcher } from "./ProjectRoleService"; export enum OrgPermissionActions { Read = "read", @@ -74,7 +75,7 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); - return build(); + return build({ conditionsMatcher }); }; export const adminPermissions = buildAdminPermission(); @@ -92,7 +93,7 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount); can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning); - return build(); + return build({ conditionsMatcher }); }; export const memberPermissions = buildMemberPermission(); @@ -119,7 +120,9 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => { if (membership.role === "member") return { permission: memberPermissions, membership }; if (membership.role === "custom") { - const permission = createMongoAbility(membership.customRole.permissions); + const permission = createMongoAbility(membership.customRole.permissions, { + conditionsMatcher + }); return { permission, membership }; } diff --git a/backend/src/validation/role.ts b/backend/src/validation/role.ts index efc659a23..e3ecafe59 100644 --- a/backend/src/validation/role.ts +++ b/backend/src/validation/role.ts @@ -11,7 +11,9 @@ export const CreateRoleSchema = z.object({ .object({ subject: z.string().trim(), action: z.string().trim(), - conditions: z.record(z.union([z.string().trim(), z.number()])).optional() + conditions: z + .record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })])) + .optional() }) .array() }) @@ -31,7 +33,9 @@ export const UpdateRoleSchema = z.object({ .object({ subject: z.string().trim(), action: z.string().trim(), - conditions: z.record(z.union([z.string().trim(), z.number()])).optional() + conditions: z + .record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })])) + .optional() }) .array() .optional() diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index afa77cbf0..700a3ea5d 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -189,7 +189,7 @@ export const BatchSecretsV2 = z.object({ workspaceId: z.string().trim(), folderId: z.string().trim().default("root"), environment: z.string().trim(), - secretPath: z.string().trim().optional(), + secretPath: z.string().trim().default("/"), requests: z .discriminatedUnion("method", [ z.object({ @@ -328,7 +328,7 @@ export const CreateSecretV3 = z.object({ secretCommentCiphertext: z.string().trim().optional(), secretCommentIV: z.string().trim().optional(), secretCommentTag: z.string().trim().optional(), - metadata: z.record(z.string()).optional(), + metadata: z.record(z.string()).optional() }), params: z.object({ secretName: z.string().trim() diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 262051bcf..e6847b42c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -41,6 +41,7 @@ "@stripe/stripe-js": "^1.46.0", "@tanstack/react-query": "^4.23.0", "@types/argon2-browser": "^1.18.1", + "@ucast/mongo2js": "^1.3.4", "add": "^2.0.6", "argon2-browser": "^1.18.0", "axios": "^0.27.2", @@ -65,6 +66,7 @@ "markdown-it": "^13.0.1", "next": "^12.3.4", "nprogress": "^0.2.0", + "picomatch": "^2.3.1", "posthog-js": "^1.58.0", "query-string": "^7.1.3", "react": "^17.0.2", @@ -106,6 +108,7 @@ "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", "@types/node": "^18.11.9", + "@types/picomatch": "^2.3.0", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", @@ -8391,6 +8394,12 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "node_modules/@types/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==", + "dev": true + }, "node_modules/@types/pretty-hrtime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz", @@ -29414,6 +29423,12 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "@types/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==", + "dev": true + }, "@types/pretty-hrtime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1e73e4c7e..d954da2a8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -49,6 +49,7 @@ "@stripe/stripe-js": "^1.46.0", "@tanstack/react-query": "^4.23.0", "@types/argon2-browser": "^1.18.1", + "@ucast/mongo2js": "^1.3.4", "add": "^2.0.6", "argon2-browser": "^1.18.0", "axios": "^0.27.2", @@ -73,6 +74,7 @@ "markdown-it": "^13.0.1", "next": "^12.3.4", "nprogress": "^0.2.0", + "picomatch": "^2.3.1", "posthog-js": "^1.58.0", "query-string": "^7.1.3", "react": "^17.0.2", @@ -114,6 +116,7 @@ "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", "@types/node": "^18.11.9", + "@types/picomatch": "^2.3.0", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", diff --git a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx index a8b965269..77ca8c24f 100644 --- a/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx +++ b/frontend/src/components/AddTagPopoverContent/AddTagPopoverContent.tsx @@ -1,78 +1,92 @@ - import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Checkbox, PopoverContent } from "@app/components/v2"; +import { Button, Checkbox, PopoverContent } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { WsTag } from "../../hooks/api/tags/types"; +import { ProjectPermissionCan } from "../permissions"; interface Props { - wsTags: WsTag[] | undefined; - secKey: string; - selectedTagIds: Record; - handleSelectTag: (wsTag: WsTag) => void; - handleTagOnMouseEnter: (wsTag: WsTag) => void; - handleTagOnMouseLeave: () => void; - checkIfTagIsVisible: (wsTag: WsTag) => boolean; - handleOnCreateTagOpen: () => void + wsTags: WsTag[] | undefined; + secKey: string; + selectedTagIds: Record; + handleSelectTag: (wsTag: WsTag) => void; + handleTagOnMouseEnter: (wsTag: WsTag) => void; + handleTagOnMouseLeave: () => void; + checkIfTagIsVisible: (wsTag: WsTag) => boolean; + handleOnCreateTagOpen: () => void; } const AddTagPopoverContent = ({ - wsTags, - secKey, - selectedTagIds, - handleSelectTag, - handleTagOnMouseEnter, - handleTagOnMouseLeave, - checkIfTagIsVisible, - handleOnCreateTagOpen + wsTags, + secKey, + selectedTagIds, + handleSelectTag, + handleTagOnMouseEnter, + handleTagOnMouseLeave, + checkIfTagIsVisible, + handleOnCreateTagOpen }: Props) => { - return ( - -
- Add tags to {secKey || "this secret"} + return ( + +
+ Add tags to {secKey || "this secret"} +
+
+
+ {wsTags?.map((wsTag: WsTag) => ( +
handleSelectTag(wsTag)} + onMouseEnter={() => handleTagOnMouseEnter(wsTag)} + onMouseLeave={() => handleTagOnMouseLeave()} + tabIndex={0} + role="button" + onKeyDown={() => {}} + > + {(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && ( + + )} +
+
+ {" "} +
+ {wsTag.slug}
-
-
- {wsTags?.map((wsTag: WsTag) => ( -
handleSelectTag(wsTag)} - onMouseEnter={() => handleTagOnMouseEnter(wsTag)} - onMouseLeave={() => handleTagOnMouseLeave()} - tabIndex={0} role="button" - onKeyDown={() => { }}> - { +
+ ))} + + {(isAllowed) => ( + + )} + +
+ + ); +}; - (checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && - } -
-
- - {wsTag.slug} - -
-
- ))} -
handleOnCreateTagOpen()} - tabIndex={0} role="button" - onKeyDown={() => { }}> - - Add new tag -
-
- - ) -} - -export default AddTagPopoverContent \ No newline at end of file +export default AddTagPopoverContent; diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx new file mode 100644 index 000000000..5e70d9e72 --- /dev/null +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -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 ( +
+
+
+ +
+
+
Permission Denied
+ {children || ( +
+ You do not have permission.
Kindly contact your organization administrator +
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/components/permissions/ProjectPermissionCan.tsx b/frontend/src/components/permissions/ProjectPermissionCan.tsx index 6c76afde9..7857ac052 100644 --- a/frontend/src/components/permissions/ProjectPermissionCan.tsx +++ b/frontend/src/components/permissions/ProjectPermissionCan.tsx @@ -11,7 +11,11 @@ type Props = { // so when permission is allowed same tooltip will be reused to show helpertext renderTooltip?: boolean; allowedLabel?: string; -} & BoundCanProps; + // 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; export const ProjectPermissionCan: FunctionComponent = ({ label = "Permission Denied. Kindly contact your project admin", @@ -22,7 +26,6 @@ export const ProjectPermissionCan: FunctionComponent = ({ ...props }) => { const permission = useProjectPermission(); - return ( {(isAllowed, ability) => { diff --git a/frontend/src/components/permissions/index.tsx b/frontend/src/components/permissions/index.tsx index 24854f047..8d523c311 100644 --- a/frontend/src/components/permissions/index.tsx +++ b/frontend/src/components/permissions/index.tsx @@ -1,2 +1,3 @@ export { OrgPermissionCan } from "./OrgPermissionCan"; +export { PermissionDeniedBanner } from "./PermissionDeniedBanner"; export { ProjectPermissionCan } from "./ProjectPermissionCan"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d81ed068e..ca1ca13ae 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -27,7 +27,7 @@ export enum ProjectPermissionSub { type SubjectFields = { environment: string; - secretPath?: string; + secretPath: string; }; export type ProjectPermissionSet = diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index 917b6d5f6..5415d201c 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -1,6 +1,8 @@ -import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; +import { buildMongoQueryMatcher, createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, unpackRules } from "@casl/ability/extra"; import { useQuery } from "@tanstack/react-query"; +import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js"; +import picomatch from "picomatch"; import { apiRequest } from "@app/config/request"; import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types"; @@ -13,6 +15,29 @@ import { TRole } from "./types"; +const $glob: FieldInstruction = { + type: "field", + validate(instruction, value) { + if (typeof value !== "string") { + throw new Error(`"${instruction.name}" expects value to be a string`); + } + } +}; + +const glob: JsInterpreter> = (node, object, context) => { + const secretPath = context.get(object, node.field); + const permissionSecretGlobPath = node.value; + if (!secretPath) return false; + // console.log( + // secretPath, + // picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }), + // permissionSecretGlobPath + // ); + return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }); +}; + +const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); + export const roleQueryKeys = { getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const, getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) => @@ -57,7 +82,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) = enabled: Boolean(orgId), select: (data) => { const rule = unpackRules>>(data); - const ability = createMongoAbility(rule); + const ability = createMongoAbility(rule, { conditionsMatcher }); return ability; } }); diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index bd4152b7c..d03161f30 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -739,16 +739,26 @@ export const AppLayout = ({ children }: LayoutProps) => { ( - - Add all members of my organization to this project - + {(isAllowed) => ( +
+ + Add all members of my organization to this project + +
+ )} + )} />
diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index ea72be394..fdd5c2cdf 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -838,16 +838,26 @@ const OrganizationPage = withPermission( ( - - Add all members of my organization to this project - + {(isAllowed) => ( +
+ + Add all members of my organization to this project + +
+ )} + )} />
diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index 5c93b6192..6e8509aba 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -3,6 +3,7 @@ import { FormProvider, useFieldArray, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; import { subject } from "@casl/ability"; +import { Can } from "@casl/react"; import { closestCenter, DndContext, @@ -26,7 +27,6 @@ import { faEyeSlash, faFileImport, faFolderPlus, - faLock, faMagnifyingGlass, faPlus } from "@fortawesome/free-solid-svg-icons"; @@ -41,7 +41,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import NavHeader from "@app/components/navigation/NavHeader"; -import { ProjectPermissionCan } from "@app/components/permissions"; +import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal, @@ -66,7 +66,6 @@ import { useSubscription, useWorkspace } from "@app/context"; -import { withProjectPermission } from "@app/hoc"; import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; import { useBatchSecretsOp, @@ -92,7 +91,6 @@ import { useUpdateSecretImport } from "@app/hooks/api"; import { secretKeys } from "@app/hooks/api/secrets/queries"; -import { WorkspaceEnv } from "@app/hooks/api/types"; import { CompareSecret } from "./components/CompareSecret"; import { CreateTagModal } from "./components/CreateTagModal"; @@ -133,944 +131,936 @@ type TDeleteSecretImport = { environment: string; secretPath: string }; * Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving * They will get it back */ -export const DashboardPage = withProjectPermission( - () => { - const { subscription } = useSubscription(); - const { t } = useTranslation(); - const router = useRouter(); - const { createNotification } = useNotificationContext(); - const queryClient = useQueryClient(); - const envQuery = router.query.env as string; - const permission = useProjectPermission(); +export const DashboardPage = () => { + const { subscription } = useSubscription(); + const { t } = useTranslation(); + const router = useRouter(); + const { createNotification } = useNotificationContext(); + const queryClient = useQueryClient(); + const environment = router.query.env as string; + const permission = useProjectPermission(); - const secretContainer = useRef(null); - const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ - "secretDetails", - "addTag", - "secretSnapshots", - "uploadedSecOpts", - "compareSecrets", - "folderForm", - "deleteFolder", - "upgradePlan", - "addSecretImport", - "deleteSecretImport" - ] as const); - const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); - const [searchFilter, setSearchFilter] = useState(""); - const [snapshotId, setSnaphotId] = useState(null); - const [selectedEnv, setSelectedEnv] = useState(null); - const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); - const deletedSecretIds = useRef<{ id: string; secretName: string }[]>([]); - const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); + const secretContainer = useRef(null); + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "secretDetails", + "addTag", + "secretSnapshots", + "uploadedSecOpts", + "compareSecrets", + "folderForm", + "deleteFolder", + "upgradePlan", + "addSecretImport", + "deleteSecretImport" + ] as const); + const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true); + const [searchFilter, setSearchFilter] = useState(""); + const [snapshotId, setSnaphotId] = useState(null); + const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); + const deletedSecretIds = useRef<{ id: string; secretName: string }[]>([]); + const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); - const folderId = router.query.folderId as string; - const isRollbackMode = Boolean(snapshotId); + const folderId = router.query.folderId as string; + const isRollbackMode = Boolean(snapshotId); - const { currentWorkspace, isLoading } = useWorkspace(); - const { currentOrg } = useOrganization(); - const workspaceId = currentWorkspace?._id as string; - const selectedEnvSlug = selectedEnv?.slug || ""; + const { currentWorkspace, isLoading } = useWorkspace(); + const { currentOrg } = useOrganization(); + const workspaceId = currentWorkspace?._id as string; - const { data: latestFileKey } = useGetUserWsKey(workspaceId); + const { data: latestFileKey } = useGetUserWsKey(workspaceId); - useEffect(() => { - if (!isLoading && !workspaceId && router.isReady) { - router.push(`/org/${currentOrg?._id}/overview`); - } - }, [isLoading, workspaceId, router.isReady]); + useEffect(() => { + if (!isLoading && !workspaceId && router.isReady) { + router.push(`/org/${currentOrg?._id}/overview`); + } + }, [isLoading, workspaceId, router.isReady]); - // fetching data - const { data: userAction } = useGetUserAction(USER_ACTION_PUSH); - const hasUserPushed = Boolean(userAction); + // fetching data + const { data: userAction } = useGetUserAction(USER_ACTION_PUSH); + const hasUserPushed = Boolean(userAction); - const { data: secretVersion } = useGetSecretVersion({ - limit: 10, - offset: 0, - secretId: (popUp?.secretDetails?.data as TSecretDetailsOpen)?.id, - decryptFileKey: latestFileKey! - }); + const { data: secretVersion } = useGetSecretVersion({ + limit: 10, + offset: 0, + secretId: (popUp?.secretDetails?.data as TSecretDetailsOpen)?.id, + decryptFileKey: latestFileKey! + }); - const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ - workspaceId, - env: selectedEnvSlug, - decryptFileKey: latestFileKey!, - isPaused: Boolean(snapshotId), - folderId - }); + const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ + workspaceId, + env: environment, + decryptFileKey: latestFileKey!, + isPaused: Boolean(snapshotId), + folderId + }); - const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({ - workspaceId: workspaceId || "", - environment: selectedEnvSlug, - parentFolderId: folderId, - isPaused: isRollbackMode, - sortDir - }); + const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({ + workspaceId: workspaceId || "", + environment, + parentFolderId: folderId, + isPaused: isRollbackMode, + sortDir + }); - const { - data: secretSnaphots, - fetchNextPage, - hasNextPage, - isFetchingNextPage - } = useGetWorkspaceSecretSnapshots({ - workspaceId, - environment: selectedEnvSlug, - folder: folderId, - limit: 10 - }); + const { + data: secretSnaphots, + fetchNextPage, + hasNextPage, + isFetchingNextPage + } = useGetWorkspaceSecretSnapshots({ + workspaceId, + environment, + folder: folderId, + limit: 10 + }); - const { - data: snapshotSecret, - isLoading: isSnapshotSecretsLoading, - isFetching: isSnapshotChanging - } = useGetSnapshotSecrets({ - snapshotId: snapshotId || "", - env: selectedEnvSlug, - decryptFileKey: latestFileKey! - }); + const { + data: snapshotSecret, + isLoading: isSnapshotSecretsLoading, + isFetching: isSnapshotChanging + } = useGetSnapshotSecrets({ + snapshotId: snapshotId || "", + env: environment, + decryptFileKey: latestFileKey! + }); - const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount( - workspaceId, - selectedEnvSlug, - folderId - ); + const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount( + workspaceId, + environment, + folderId + ); - const { data: wsTags } = useGetWsTags(workspaceId); + const { data: wsTags } = useGetWsTags(workspaceId); - // mutation calls - const { mutateAsync: batchSecretOp } = useBatchSecretsOp(); - const { mutateAsync: performSecretRollback } = usePerformSecretRollback(); - const { mutateAsync: registerUserAction } = useRegisterUserAction(); - const { mutateAsync: createWsTag } = useCreateWsTag(); - const { mutateAsync: createFolder } = useCreateFolder(); - const { mutateAsync: updateFolder } = useUpdateFolder(folderId); - const { mutateAsync: deleteFolder } = useDeleteFolder(folderId); + // mutation calls + const { mutateAsync: batchSecretOp } = useBatchSecretsOp(); + const { mutateAsync: performSecretRollback } = usePerformSecretRollback(); + const { mutateAsync: registerUserAction } = useRegisterUserAction(); + const { mutateAsync: createWsTag } = useCreateWsTag(); + const { mutateAsync: createFolder } = useCreateFolder(); + const { mutateAsync: updateFolder } = useUpdateFolder(folderId); + const { mutateAsync: deleteFolder } = useDeleteFolder(folderId); - const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports( - workspaceId, - selectedEnvSlug, - folderId - ); + const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports( + workspaceId, + environment, + folderId + ); - const { data: importedSecrets } = useGetImportedSecrets({ - workspaceId, - decryptFileKey: latestFileKey!, - environment: selectedEnvSlug, - folderId - }); + const { data: importedSecrets } = useGetImportedSecrets({ + workspaceId, + decryptFileKey: latestFileKey!, + environment, + folderId + }); - const secretPath = `/${(folderData?.dir || []) - ?.filter(({ name }) => name !== "root") - .join("/")}`; + const secretPath = `/${(folderData?.dir || []) + ?.filter(({ name }) => name !== "root") + ?.map(({ name }) => name) + .join("/")}`; - const userAvailableEnvs = currentWorkspace?.environments?.filter(({ slug }) => + const userAvailableEnvs = currentWorkspace?.environments?.filter( + ({ slug }) => permission.can( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath }) - ) - ); - - useEffect(() => { - if (!isLoading && currentWorkspace) { - const env = userAvailableEnvs?.find(({ slug }) => slug === envQuery); - if (env) setSelectedEnv(env); - } - }, [isLoading, workspaceId, userAvailableEnvs]); - - // This is for dnd-kit. As react-query state mutation async - // This will act as a placeholder to avoid a glitching animation on dropping items - const [items, setItems] = useState< - Array<{ environment: string; secretPath: string; id: string }> - >([]); - - useEffect(() => { - if ( - !isSecretImportCfgFetching || - // case in which u go to a folder and come back to fill in with cache data - (items.length === 0 && secretImportCfg?.imports?.length !== 0 && isSecretImportCfgFetching) - ) { - setItems( - secretImportCfg?.imports?.map((el) => ({ - ...el, - id: `${el.environment}-${el.secretPath}` - })) || [] - ); - } - }, [isSecretImportCfgFetching]); - - const { mutateAsync: createSecretImport } = useCreateSecretImport(); - const { mutate: updateSecretImportSync } = useUpdateSecretImport(); - const { mutateAsync: deleteSecretImport } = useDeleteSecretImport(); - - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) - ); - - const method = useForm({ - // why any: well yup inferred ts expects other keys to defined as undefined - defaultValues: secrets as any, - values: secrets as any, - mode: "onBlur", - resolver: yupResolver(schema) - }); - - const { - register, - control, - handleSubmit, - getValues, - setValue, - formState: { isSubmitting, isDirty, errors }, - reset - } = method; - const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" }); - - const isReadOnly = + ) || permission.can( ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: selectedEnvSlug }) + subject(ProjectPermissionSub.Folders, { environment: slug, secretPath }) + ) || + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment: slug, secretPath }) + ) + ); + + // This is for dnd-kit. As react-query state mutation async + // This will act as a placeholder to avoid a glitching animation on dropping items + const [items, setItems] = useState< + Array<{ environment: string; secretPath: string; id: string }> + >([]); + + useEffect(() => { + if ( + !isSecretImportCfgFetching || + // case in which u go to a folder and come back to fill in with cache data + (items.length === 0 && secretImportCfg?.imports?.length !== 0 && isSecretImportCfgFetching) + ) { + setItems( + secretImportCfg?.imports?.map((el) => ({ + ...el, + id: `${el.environment}-${el.secretPath}` + })) || [] + ); + } + }, [isSecretImportCfgFetching]); + + const { mutateAsync: createSecretImport } = useCreateSecretImport(); + const { mutate: updateSecretImportSync } = useUpdateSecretImport(); + const { mutateAsync: deleteSecretImport } = useDeleteSecretImport(); + + const sensors = useSensors( + useSensor(MouseSensor, {}), + useSensor(TouchSensor, {}), + useSensor(KeyboardSensor, {}) + ); + + const method = useForm({ + // why any: well yup inferred ts expects other keys to defined as undefined + defaultValues: secrets as any, + values: secrets as any, + mode: "onBlur", + resolver: yupResolver(schema) + }); + + const { + register, + control, + handleSubmit, + getValues, + setValue, + formState: { isSubmitting, isDirty, errors }, + reset + } = method; + const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" }); + + const isReadOnly = isFoldersLoading + ? true + : permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ) && permission.cannot( ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment: selectedEnvSlug }) + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) ); - const canDoRollback = !isReadOnly; - const isSubmitDisabled = isReadOnly || (!isRollbackMode && !isDirty) || isSubmitting; + const canDoRollback = !isReadOnly; + const isSubmitDisabled = isReadOnly || (!isRollbackMode && !isDirty) || isSubmitting; - useEffect(() => { - if (!isSnapshotChanging && Boolean(snapshotId)) { - reset({ secrets: snapshotSecret?.secrets, isSnapshotMode: true }); - } - }, [isSnapshotChanging]); + useEffect(() => { + if (!isSnapshotChanging && Boolean(snapshotId)) { + reset({ secrets: snapshotSecret?.secrets, isSnapshotMode: true }); + } + }, [isSnapshotChanging]); - useEffect(() => { - setHasUnsavedChanges(!isSubmitDisabled); - }, [isSubmitDisabled]); + useEffect(() => { + setHasUnsavedChanges(!isSubmitDisabled); + }, [isSubmitDisabled]); - const onSortSecrets = () => { - const dir = sortDir === "asc" ? "desc" : "asc"; - const sec = getValues("secrets") || []; - const sortedSec = sec.sort((a, b) => - dir === "asc" ? a?.key?.localeCompare(b?.key || "") : b?.key?.localeCompare(a?.key || "") - ); - setValue("secrets", sortedSec); - setSortDir(dir); - }; + const onSortSecrets = () => { + const dir = sortDir === "asc" ? "desc" : "asc"; + const sec = getValues("secrets") || []; + const sortedSec = sec.sort((a, b) => + dir === "asc" ? a?.key?.localeCompare(b?.key || "") : b?.key?.localeCompare(a?.key || "") + ); + setValue("secrets", sortedSec); + setSortDir(dir); + }; - const handleUploadedEnv = (uploadedSec: TSecOverwriteOpt["secrets"]) => { - const sec = getValues("secrets") || []; - const conflictingSec = sec.filter(({ key }) => Boolean(uploadedSec?.[key])); - const conflictingSecIds = conflictingSec.reduce>( - (prev, curr) => ({ - ...prev, - [curr.key]: true - }), - {} - ); - // filter to get all conflicting ones - const conflictingUploadedSec = { ...uploadedSec }; - // append non conflicting ones - Object.keys(uploadedSec).forEach((key) => { - if (!conflictingSecIds?.[key]) { - delete conflictingUploadedSec[key]; - sec.push({ - ...DEFAULT_SECRET_VALUE, - key, - value: uploadedSec[key].value, - comment: uploadedSec[key].comments.join(",") - }); - } - }); - setValue("secrets", sec, { shouldDirty: true }); - if (conflictingSec.length > 0) { - handlePopUpOpen("uploadedSecOpts", { secrets: conflictingUploadedSec }); - } - }; - - const onOverwriteSecrets = () => { - const sec = getValues("secrets") || []; - const uploadedSec = (popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets; - const data: Array<{ key: string; index: number }> = []; - sec.forEach(({ key }, index) => { - if (uploadedSec?.[key]) data.push({ key, index }); - }); - data.forEach(({ key, index }) => { - const { value, comments } = uploadedSec[key]; - const comment = comments.join(", "); - sec[index] = { + const handleUploadedEnv = (uploadedSec: TSecOverwriteOpt["secrets"]) => { + const sec = getValues("secrets") || []; + const conflictingSec = sec.filter(({ key }) => Boolean(uploadedSec?.[key])); + const conflictingSecIds = conflictingSec.reduce>( + (prev, curr) => ({ + ...prev, + [curr.key]: true + }), + {} + ); + // filter to get all conflicting ones + const conflictingUploadedSec = { ...uploadedSec }; + // append non conflicting ones + Object.keys(uploadedSec).forEach((key) => { + if (!conflictingSecIds?.[key]) { + delete conflictingUploadedSec[key]; + sec.push({ ...DEFAULT_SECRET_VALUE, key, - value, - comment, - tags: sec[index].tags - }; + value: uploadedSec[key].value, + comment: uploadedSec[key].comments.join(",") + }); + } + }); + setValue("secrets", sec, { shouldDirty: true }); + if (conflictingSec.length > 0) { + handlePopUpOpen("uploadedSecOpts", { secrets: conflictingUploadedSec }); + } + }; + + const onOverwriteSecrets = () => { + const sec = getValues("secrets") || []; + const uploadedSec = (popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets; + const data: Array<{ key: string; index: number }> = []; + sec.forEach(({ key }, index) => { + if (uploadedSec?.[key]) data.push({ key, index }); + }); + data.forEach(({ key, index }) => { + const { value, comments } = uploadedSec[key]; + const comment = comments.join(", "); + sec[index] = { + ...DEFAULT_SECRET_VALUE, + key, + value, + comment, + tags: sec[index].tags + }; + }); + setValue("secrets", sec, { shouldDirty: true }); + handlePopUpClose("uploadedSecOpts"); + }; + + const onSecretRollback = async () => { + if (!snapshotSecret?.version) { + createNotification({ + text: "Failed to find secret version", + type: "success" }); - setValue("secrets", sec, { shouldDirty: true }); - handlePopUpClose("uploadedSecOpts"); - }; + return; + } + try { + await performSecretRollback({ + workspaceId, + version: snapshotSecret.version, + environment, + folderId + }); + setValue("isSnapshotMode", false); + setSnaphotId(null); + queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, environment)); + createNotification({ + text: "Successfully rollback secrets", + type: "success" + }); + } catch (error) { + console.log(error); + createNotification({ + text: "Failed to rollback secrets", + type: "error" + }); + } + }; - const onSecretRollback = async () => { - if (!snapshotSecret?.version) { - createNotification({ - text: "Failed to find secret version", - type: "success" - }); - return; - } - try { - await performSecretRollback({ - workspaceId, - version: snapshotSecret.version, - environment: selectedEnvSlug, - folderId - }); - setValue("isSnapshotMode", false); - setSnaphotId(null); - queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug)); - createNotification({ - text: "Successfully rollback secrets", - type: "success" - }); - } catch (error) { - console.log(error); - createNotification({ - text: "Failed to rollback secrets", - type: "error" - }); - } - }; + const onAppendSecret = () => { + setSearchFilter(""); + append(DEFAULT_SECRET_VALUE); + }; - const onAppendSecret = () => { - setSearchFilter(""); - append(DEFAULT_SECRET_VALUE); - }; - - const onSaveSecret = async ({ secrets: userSec = [], isSnapshotMode }: FormData) => { - if (isSnapshotMode) { - await onSecretRollback(); - return; + const onSaveSecret = async ({ secrets: userSec = [], isSnapshotMode }: FormData) => { + if (isSnapshotMode) { + await onSecretRollback(); + return; + } + // just closing this if save is triggered from drawer + handlePopUpClose("secretDetails"); + // encrypt and format the secrets to batch api format + // requests = [ {method:"", secret:""} ] + const batchedSecret = transformSecretsToBatchSecretReq( + deletedSecretIds.current, + latestFileKey, + userSec, + secrets?.secrets + ); + // type check + if (batchedSecret.length === 0) { + reset(); + return; + } + try { + await batchSecretOp({ + requests: batchedSecret, + workspaceId, + folderId, + environment + }); + createNotification({ + text: "Successfully saved changes", + type: "success" + }); + deletedSecretIds.current = []; + if (!hasUserPushed) { + await registerUserAction(USER_ACTION_PUSH); } - // just closing this if save is triggered from drawer + } catch (error) { + console.log(error); + createNotification({ + text: "Failed to save changes", + type: "error" + }); + } + }; + + const onDrawerOpen = useCallback((id: string | undefined, index: number) => { + handlePopUpOpen("secretDetails", { id, index } as TSecretDetailsOpen); + }, []); + + const onEnvChange = (slug: string) => { + if (hasUnsavedChanges) { + // eslint-disable-next-line no-alert + if (!window.confirm(leaveConfirmDefaultMessage)) return; + } + + const query: Record = { ...router.query, env: slug }; + delete query.folderId; + router.push({ + pathname: router.pathname, + query + }); + }; + + const handleDownloadSecret = () => { + const secretsFromImport: { key: string; value: string; comment: string }[] = []; + importedSecrets?.forEach(({ secrets: impSec }) => { + impSec.forEach((el) => { + secretsFromImport.push({ key: el.key, value: el.value, comment: el.comment }); + }); + }); + downloadSecret(getValues("secrets"), secretsFromImport, environment); + }; + + // record all deleted ids + // This will make final deletion easier + const onSecretDelete = useCallback( + (index: number, secretName: string, id?: string, overrideId?: string) => { + if (id) + deletedSecretIds.current.push({ + id, + secretName + }); + if (overrideId) + deletedSecretIds.current.push({ + id: overrideId, + secretName + }); + remove(index); + // just the case if this is called from drawer handlePopUpClose("secretDetails"); - // encrypt and format the secrets to batch api format - // requests = [ {method:"", secret:""} ] - const batchedSecret = transformSecretsToBatchSecretReq( - deletedSecretIds.current, - latestFileKey, - userSec, - secrets?.secrets - ); - // type check - if (!selectedEnv?.slug) return; - if (batchedSecret.length === 0) { - reset(); - return; - } + }, + [] + ); + + const onCreateWsTag = useCallback( + async (tagName: string, tagColor: string) => { try { - await batchSecretOp({ - requests: batchedSecret, - workspaceId, - folderId, - environment: selectedEnv?.slug + await createWsTag({ + workspaceID: workspaceId, + tagName, + tagColor, + tagSlug: tagName.replace(" ", "_") }); + handlePopUpClose("addTag"); createNotification({ - text: "Successfully saved changes", + text: "Successfully created a tag", type: "success" }); - deletedSecretIds.current = []; - if (!hasUserPushed) { - await registerUserAction(USER_ACTION_PUSH); - } } catch (error) { - console.log(error); + console.error(error); createNotification({ - text: "Failed to save changes", + text: "Failed to create a tag", type: "error" }); } - }; + }, + [workspaceId] + ); - const onDrawerOpen = useCallback((id: string | undefined, index: number) => { - handlePopUpOpen("secretDetails", { id, index } as TSecretDetailsOpen); - }, []); - - const onEnvChange = (slug: string) => { - if (hasUnsavedChanges) { - // eslint-disable-next-line no-alert - if (!window.confirm(leaveConfirmDefaultMessage)) return; - } - - const env = userAvailableEnvs?.find((el) => el.slug === slug); - if (env) setSelectedEnv(env); - const query: Record = { ...router.query, env: slug }; - delete query.folderId; + const handleFolderOpen = useCallback( + (id: string) => { + setSearchFilter(""); router.push({ pathname: router.pathname, - query - }); - }; - - const handleDownloadSecret = () => { - const secretsFromImport: { key: string; value: string; comment: string }[] = []; - importedSecrets?.forEach(({ secrets: impSec }) => { - impSec.forEach((el) => { - secretsFromImport.push({ key: el.key, value: el.value, comment: el.comment }); - }); - }); - downloadSecret(getValues("secrets"), secretsFromImport, selectedEnv?.slug); - }; - - // record all deleted ids - // This will make final deletion easier - const onSecretDelete = useCallback( - (index: number, secretName: string, id?: string, overrideId?: string) => { - if (id) - deletedSecretIds.current.push({ - id, - secretName - }); - if (overrideId) - deletedSecretIds.current.push({ - id: overrideId, - secretName - }); - remove(index); - // just the case if this is called from drawer - handlePopUpClose("secretDetails"); - }, - [] - ); - - const onCreateWsTag = useCallback( - async (tagName: string, tagColor: string) => { - try { - await createWsTag({ - workspaceID: workspaceId, - tagName, - tagColor, - tagSlug: tagName.replace(" ", "_") - }); - handlePopUpClose("addTag"); - createNotification({ - text: "Successfully created a tag", - type: "success" - }); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to create a tag", - type: "error" - }); + query: { + id: workspaceId, + env: environment, + folderId: id } - }, - [workspaceId] - ); + }); + }, + [environment, workspaceId] + ); - const handleFolderOpen = useCallback( - (id: string) => { - setSearchFilter(""); - router.push({ - pathname: router.pathname, - query: { - id: workspaceId, - env: envQuery, - folderId: id - } - }); - }, - [envQuery, workspaceId] - ); + const isEditFolder = Boolean(popUp?.folderForm?.data); - const isEditFolder = Boolean(popUp?.folderForm?.data); + // FOLDER SECTION + const handleFolderCreate = async (name: string) => { + try { + await createFolder({ + workspaceId, + environment, + folderName: name, + parentFolderId: folderId + }); + createNotification({ + type: "success", + text: "Successfully created folder" + }); + handlePopUpClose("folderForm"); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to create folder", + type: "error" + }); + } + }; - // FOLDER SECTION - const handleFolderCreate = async (name: string) => { + const handleFolderUpdate = useCallback( + async (name: string) => { + const { id } = popUp?.folderForm?.data as TDeleteFolderForm; try { - await createFolder({ + await updateFolder({ + folderId: id, workspaceId, - environment: selectedEnv?.slug || "", - folderName: name, - parentFolderId: folderId + environment, + name }); createNotification({ type: "success", - text: "Successfully created folder" + text: "Successfully updated folder" }); handlePopUpClose("folderForm"); } catch (error) { console.error(error); createNotification({ - text: "Failed to create folder", + text: "Failed to update folder", type: "error" }); } - }; + }, + [environment, (popUp?.folderForm?.data as TDeleteFolderForm)?.id] + ); - const handleFolderUpdate = useCallback( - async (name: string) => { - const { id } = popUp?.folderForm?.data as TDeleteFolderForm; - try { - await updateFolder({ - folderId: id, - workspaceId, - environment: selectedEnv?.slug || "", - name - }); - createNotification({ - type: "success", - text: "Successfully updated folder" - }); - handlePopUpClose("folderForm"); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to update folder", - type: "error" - }); + const handleFolderDelete = useCallback(async () => { + const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm; + try { + deleteFolder({ + workspaceId, + environment, + folderId: id + }); + createNotification({ + type: "success", + text: "Successfully removed folder" + }); + handlePopUpClose("deleteFolder"); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to remove folder", + type: "error" + }); + } + }, [(popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]); + + // SECRET IMPORT SECTION + const handleSecretImportCreate = async (env: string, secPath: string) => { + try { + await createSecretImport({ + workspaceId, + environment, + folderId, + secretImport: { + environment: env, + secretPath: secPath } - }, - [selectedEnv?.slug, (popUp?.folderForm?.data as TDeleteFolderForm)?.id] - ); + }); + createNotification({ + type: "success", + text: "Successfully create secret link" + }); + handlePopUpClose("addSecretImport"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to create secret link", + type: "error" + }); + } + }; - const handleFolderDelete = useCallback(async () => { - const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm; - try { - deleteFolder({ + const handleSecretImportDelete = async () => { + const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport + ?.data as TDeleteSecretImport; + try { + if (secretImportCfg?._id) { + await deleteSecretImport({ workspaceId, - environment: selectedEnv?.slug || "", - folderId: id + environment, + folderId, + id: secretImportCfg?._id, + secretImportEnv: importEnv, + secretImportPath: impSecPath }); + handlePopUpClose("deleteSecretImport"); createNotification({ type: "success", - text: "Successfully removed folder" - }); - handlePopUpClose("deleteFolder"); - } catch (error) { - console.error(error); - createNotification({ - text: "Failed to remove folder", - type: "error" + text: "Successfully removed secret link" }); } - }, [selectedEnv?.slug, (popUp?.deleteFolder?.data as TDeleteFolderForm)?.id]); - - // SECRET IMPORT SECTION - const handleSecretImportCreate = async (env: string, secPath: string) => { - try { - await createSecretImport({ - workspaceId, - environment: selectedEnv?.slug || "", - folderId, - secretImport: { - environment: env, - secretPath: secPath - } - }); - createNotification({ - type: "success", - text: "Successfully create secret link" - }); - handlePopUpClose("addSecretImport"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to create secret link", - type: "error" - }); - } - }; - - const handleSecretImportDelete = async () => { - const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport - ?.data as TDeleteSecretImport; - try { - if (secretImportCfg?._id) { - await deleteSecretImport({ - workspaceId, - environment: selectedEnvSlug, - folderId, - id: secretImportCfg?._id, - secretImportEnv: importEnv, - secretImportPath: impSecPath - }); - handlePopUpClose("deleteSecretImport"); - createNotification({ - type: "success", - text: "Successfully removed secret link" - }); - } - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to remove secret link", - type: "error" - }); - } - }; - - const handleDragEnd = (evt: DragEndEvent) => { - const { active, over } = evt; - if (over?.id && active.id !== over.id) { - const oldIndex = items.findIndex(({ id }) => id === active.id); - const newIndex = items.findIndex(({ id }) => id === over.id); - const newImportOrder = arrayMove(items, oldIndex, newIndex); - setItems(newImportOrder); - updateSecretImportSync({ - workspaceId, - environment: selectedEnvSlug, - folderId, - id: secretImportCfg?._id || "", - secretImports: newImportOrder.map((el) => ({ - environment: el.environment, - secretPath: el.secretPath - })) - }); - } - }; - - // OPTIMIZATION HOOKS PURELY FOR PERFORMANCE AND TO AVOID RE-RENDERING - const handleCreateTagModalOpen = useCallback(() => handlePopUpOpen("addTag"), []); - const handleFolderCreatePopUpOpen = useCallback( - (id: string, name: string) => handlePopUpOpen("folderForm", { id, name }), - [] - ); - const handleFolderDeletePopUpOpen = useCallback( - (id: string, name: string) => handlePopUpOpen("deleteFolder", { id, name }), - [] - ); - const handleSecretImportDelPopUpOpen = useCallback( - (impSecEnv: string, impSecPath: string) => - handlePopUpOpen("deleteSecretImport", { - environment: impSecEnv, - secretPath: impSecPath - }), - [] - ); - - // when secrets is not loading and secrets list is empty - const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length; - - // folder list checks - const isFolderListLoading = isRollbackMode ? isSnapshotSecretsLoading : isFoldersLoading; - const folderList = isRollbackMode ? snapshotSecret?.folders : folderData?.folders; - - // when using snapshot mode and snapshot is loading and snapshot list is empty - const isFoldersEmpty = !isFolderListLoading && !folderList?.length; - const isSnapshotSecretEmtpy = - isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length; - const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy; - const isSecretImportEmpty = !secretImportCfg?.imports?.length; - const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty; - - if (isSecretsLoading) { - return ( -
- loading animation -
- ); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to remove secret link", + type: "error" + }); } + }; - if ( - permission.cannot( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment: envQuery, secretPath }) - ) - ) { - return ( -
-
-
- -
-
-
Permission Denied
-
- You do not have permission to this page.
Kindly contact your organization - administrator -
-
-
-
- ); + const handleDragEnd = (evt: DragEndEvent) => { + const { active, over } = evt; + if (over?.id && active.id !== over.id) { + const oldIndex = items.findIndex(({ id }) => id === active.id); + const newIndex = items.findIndex(({ id }) => id === over.id); + const newImportOrder = arrayMove(items, oldIndex, newIndex); + setItems(newImportOrder); + updateSecretImportSync({ + workspaceId, + environment, + folderId, + id: secretImportCfg?._id || "", + secretImports: newImportOrder.map((el) => ({ + environment: el.environment, + secretPath: el.secretPath + })) + }); } + }; + // OPTIMIZATION HOOKS PURELY FOR PERFORMANCE AND TO AVOID RE-RENDERING + const handleCreateTagModalOpen = useCallback(() => handlePopUpOpen("addTag"), []); + const handleFolderCreatePopUpOpen = useCallback( + (id: string, name: string) => handlePopUpOpen("folderForm", { id, name }), + [] + ); + const handleFolderDeletePopUpOpen = useCallback( + (id: string, name: string) => handlePopUpOpen("deleteFolder", { id, name }), + [] + ); + const handleSecretImportDelPopUpOpen = useCallback( + (impSecEnv: string, impSecPath: string) => + handlePopUpOpen("deleteSecretImport", { + environment: impSecEnv, + secretPath: impSecPath + }), + [] + ); + + // when secrets is not loading and secrets list is empty + const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length; + + // folder list checks + const isFolderListLoading = isRollbackMode ? isSnapshotSecretsLoading : isFoldersLoading; + const folderList = isRollbackMode ? snapshotSecret?.folders : folderData?.folders; + + // when using snapshot mode and snapshot is loading and snapshot list is empty + const isFoldersEmpty = !isFolderListLoading && !folderList?.length; + const isSnapshotSecretEmtpy = + isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length; + const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy; + const isSecretImportEmpty = !secretImportCfg?.imports?.length; + const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty; + + if (isSecretsLoading) { return ( -
-
- {/* breadcrumb row */} -
- envir.slug === envQuery)?.[0]?.name || "" - } - isFolderMode - folders={folderData?.dir} - isProjectRelated - userAvailableEnvs={userAvailableEnvs} - onEnvChange={onEnvChange} +
+ loading animation +
+ ); + } + + return ( +
+ + {/* breadcrumb row */} +
+ envir.slug === environment)?.[0]?.name || "" + } + isFolderMode + folders={folderData?.dir} + isProjectRelated + userAvailableEnvs={userAvailableEnvs} + onEnvChange={onEnvChange} + /> +
+
+
{isRollbackMode ? "Secret Snapshot" : ""}
+ {isRollbackMode && Boolean(snapshotSecret) && ( + + {new Date(snapshotSecret?.createdAt || "").toLocaleString()} + + )} +
+ {/* Environment, search and other action row */} +
+
+ setSearchFilter(e.target.value)} + leftIcon={} />
-
-
{isRollbackMode ? "Secret Snapshot" : ""}
- {isRollbackMode && Boolean(snapshotSecret) && ( - - {new Date(snapshotSecret?.createdAt || "").toLocaleString()} - - )} -
- {/* Environment, search and other action row */} -
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
-
-
- - - - - - - -
- -
-
-
-
-
- - setIsSecretValueHidden.toggle()} - > - +
+
+ + + + - -
- - {(isAllowed) => ( -
- - handlePopUpOpen("secretSnapshots")} - > - - - -
- )} -
- - {(isAllowed) => ( -
+ + +
- )} - - {!isReadOnly && !isRollbackMode && ( -
- - {(isAllowed) => ( - - )} - - - -
- -
-
- -
-
- - {(isAllowed) => ( - - )} - -
-
- - {(isAllowed) => ( - - )} - -
-
-
-
+ + +
+
+ + setIsSecretValueHidden.toggle()} + > + + + +
+ + {(isAllowed) => ( +
+ + handlePopUpOpen("secretSnapshots")} + > + + +
)} - {isRollbackMode && ( - +
+ )} +
+ {!isReadOnly && !isRollbackMode && ( +
+ - Go back + {(isAllowed) => ( + + )} + + + +
+ +
+
+ +
+
+ + {(isAllowed) => ( + + )} + +
+
+ + {(isAllowed) => ( + + )} + +
+
+
+
+
+ )} + {isRollbackMode && ( + + )} + + {(isAllowed) => ( + )} - - {(isAllowed) => ( - - )} - -
+
-
- {!isEmptyPage && ( - - - - - - - - {fields.map(({ id, _id }, index) => ( + +
+ {!isEmptyPage && ( + + +
+ + + + + {permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ) ? ( + fields.map(({ id, _id }, index) => ( - ))} - {!isReadOnly && !isRollbackMode && ( - - - - )} - -
- - {(isAllowed) => ( - - )} - -
-
-
- )} - - handlePopUpToggle("secretSnapshots", isOpen)} - fetchNextPage={fetchNextPage} - hasNextPage={hasNextPage} - snapshotId={snapshotId} - isFetchingNextPage={isFetchingNextPage} - secretSnaphots={secretSnaphots} - onSelectSnapshot={setSnaphotId} - /> - handlePopUpToggle("secretDetails", isOpen)} - secretVersion={secretVersion} - index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index} - onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)} - /> - - + + + + + )} + {!isReadOnly && !isRollbackMode && ( + + + + {(isAllowed) => ( + + )} + + + + )} + + + + + )} + + handlePopUpToggle("secretSnapshots", isOpen)} + fetchNextPage={fetchNextPage} + hasNextPage={hasNextPage} + snapshotId={snapshotId} + isFetchingNextPage={isFetchingNextPage} + secretSnaphots={secretSnaphots} + onSelectSnapshot={setSnaphotId} /> -
- {/* secrets table and drawers, modals */} - - {/* Create a new tag modal */} - { - handlePopUpToggle("addTag", open); - }} - > - - - - - {/* Uploaded env override or not confirmation modal */} - handlePopUpToggle("uploadedSecOpts", open)} - > - handlePopUpClose("uploadedSecOpts")} - > - Keep old - , - - ]} - > -
-
Your file contains following duplicate secrets
-
- {Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {}) - ?.map((key) => key) - .join(", ")} -
-
Are you sure you want to overwrite these secrets?
-
-
-
- handlePopUpToggle("folderForm", isOpen)} - > - - handlePopUpToggle("secretDetails", isOpen)} + secretVersion={secretVersion} + index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index} + onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)} /> - - - handlePopUpToggle("addSecretImport", isOpen)} - > - - - - - handlePopUpToggle("deleteFolder", isOpen)} - onDeleteApproved={handleFolderDelete} - /> - handlePopUpToggle("deleteSecretImport", isOpen)} - onDeleteApproved={handleSecretImportDelete} - /> - handlePopUpToggle("compareSecrets", open)} - > - - - - - {subscription && ( - handlePopUpToggle("upgradePlan", isOpen)} - text={ - subscription.slug === null - ? "You can perform point-in-time recovery under an Enterprise license" - : "You can perform point-in-time recovery if you switch to Infisical's Team plan" - } + + - )} -
- ); - }, - { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Secrets } -); +
+ {/* secrets table and drawers, modals */} + + {/* Create a new tag modal */} + { + handlePopUpToggle("addTag", open); + }} + > + + + + + {/* Uploaded env override or not confirmation modal */} + handlePopUpToggle("uploadedSecOpts", open)} + > + handlePopUpClose("uploadedSecOpts")} + > + Keep old + , + + ]} + > +
+
Your file contains following duplicate secrets
+
+ {Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {}) + ?.map((key) => key) + .join(", ")} +
+
Are you sure you want to overwrite these secrets?
+
+
+
+ handlePopUpToggle("folderForm", isOpen)} + > + + + + + handlePopUpToggle("addSecretImport", isOpen)} + > + + + + + handlePopUpToggle("deleteFolder", isOpen)} + onDeleteApproved={handleFolderDelete} + /> + handlePopUpToggle("deleteSecretImport", isOpen)} + onDeleteApproved={handleSecretImportDelete} + /> + handlePopUpToggle("compareSecrets", open)} + > + + + + + {subscription && ( + handlePopUpToggle("upgradePlan", isOpen)} + text={ + subscription.slug === null + ? "You can perform point-in-time recovery under an Enterprise license" + : "You can perform point-in-time recovery if you switch to Infisical's Team plan" + } + /> + )} +
+ ); +}; diff --git a/frontend/src/views/DashboardPage/components/FolderSection/FolderSection.tsx b/frontend/src/views/DashboardPage/components/FolderSection/FolderSection.tsx index 4fa6df286..516bb74b4 100644 --- a/frontend/src/views/DashboardPage/components/FolderSection/FolderSection.tsx +++ b/frontend/src/views/DashboardPage/components/FolderSection/FolderSection.tsx @@ -1,4 +1,5 @@ import { memo } from "react"; +import { subject } from "@casl/ability"; import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -9,6 +10,8 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; type Props = { folders?: Array<{ id: string; name: string }>; search?: string; + environment: string; + secretPath: string; onFolderUpdate: (folderId: string, name: string) => void; onFolderDelete: (folderId: string, name: string) => void; onFolderOpen: (folderId: string) => void; @@ -20,7 +23,9 @@ export const FolderSection = memo( onFolderDelete: handleFolderDelete, onFolderOpen: handleFolderOpen, search = "", - folders = [] + folders = [], + environment, + secretPath }: Props) => { return ( <> @@ -51,7 +56,7 @@ export const FolderSection = memo(
{(isAllowed) => (
@@ -72,7 +77,7 @@ export const FolderSection = memo( {(isAllowed) => (
diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx index 9668fe819..ef85c4896 100644 --- a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx +++ b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx @@ -1,4 +1,5 @@ import { useFormContext, useWatch } from "react-hook-form"; +import { subject } from "@casl/ability"; import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -23,6 +24,8 @@ import { GenRandomNumber } from "./GenRandomNumber"; type Props = { isDrawerOpen: boolean; + environment: string; + secretPath: string; onOpenChange: (isOpen: boolean) => void; index: number; isReadOnly?: boolean; @@ -41,7 +44,9 @@ export const SecretDetailDrawer = ({ isReadOnly, onSecretDelete, onSave, - onEnvCompare + onEnvCompare, + environment, + secretPath }: Props): JSX.Element => { const [canRevealSecVal, setCanRevealSecVal] = useToggle(); const [canRevealSecOverride, setCanRevealSecOverride] = useToggle(); @@ -89,7 +94,7 @@ export const SecretDetailDrawer = ({
{(isAllowed) => (
{(isAllowed) => ( - - {(isAllowed) => ( - - )} - +
+ + {(isAllowed) => ( + + )} + +
{(isAllowed) => (
{(isAllowed) => (
diff --git a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx index 9d796d079..db12e84a4 100644 --- a/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx +++ b/frontend/src/views/DashboardPage/components/SecretImportSection/SecretImportSection.tsx @@ -59,13 +59,23 @@ export const computeImportedSecretRows = ( type Props = { secrets?: DecryptedSecret[]; importedSecrets?: TImportedSecrets; + environment: string; + secretPath: string; onSecretImportDelete: (env: string, secPath: string) => void; items: { id: string; environment: string; secretPath: string }[]; searchTerm: string; }; export const SecretImportSection = memo( - ({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [], searchTerm = "" }: Props) => { + ({ + secrets = [], + environment, + secretPath, + importedSecrets = [], + onSecretImportDelete, + items = [], + searchTerm = "" + }: Props) => { const { currentWorkspace } = useWorkspace(); const environments = currentWorkspace?.environments || []; @@ -82,6 +92,8 @@ export const SecretImportSection = memo( secrets, environments )} + secretPath={secretPath} + environment={environment} onDelete={onSecretImportDelete} importedSecPath={impSecPath} searchTerm={searchTerm} diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 307cc981b..c31c6df2e 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -8,6 +8,7 @@ import { UseFormSetValue, useWatch } from "react-hook-form"; +import { subject } from "@casl/ability"; import { faCheck, faCodeBranch, @@ -22,31 +23,34 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { cx } from "cva"; import { twMerge } from "tailwind-merge"; +// TODO:(akhilmhdh): Refactor this +import AddTagPopoverContent from "@app/components/AddTagPopoverContent/AddTagPopoverContent"; import { ProjectPermissionCan } from "@app/components/permissions"; import { + FormControl, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, Input, Popover, + PopoverContent, PopoverTrigger, SecretInput, Tag, + TextArea, Tooltip } from "@app/components/v2"; -import { - ProjectPermissionActions, - ProjectPermissionSub -} from "@app/context/ProjectPermissionContext/types"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; import { WsTag } from "@app/hooks/api/types"; -import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; type Props = { index: number; + environment: string; + secretPath: string; // backend generated unique id secUniqId?: string; // permission and external state's that decided to hide or show @@ -74,6 +78,8 @@ type Props = { export const SecretInputRow = memo( ({ index, + secretPath, + environment, isSecretValueHidden, onRowExpand, isReadOnly, @@ -84,7 +90,7 @@ export const SecretInputRow = memo( onSecretDelete, searchTerm, control, - // register, + register, setValue, isKeyError, keyError, @@ -222,7 +228,6 @@ export const SecretInputRow = memo(
{index + 1}
-
- - - - - + + {(isAllowed) => ( + + + + )} +
{!isAddOnly && (
- - -
- -
-
-
+ + {(isAllowed) => ( + +
+ +
+
+ )} +
)} - -
- - - + + +
+ - - - - onSelectTag(wsTag)} - handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} - handleTagOnMouseLeave={() => handleTagOnMouseLeave()} - checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} - handleOnCreateTagOpen={() => onCreateTagOpen()} - /> - -
- + {(isAllowed) => ( + + + + )} + +
+ + + +