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

This commit is contained in:
Akhil Mohan
2023-09-07 15:20:05 +05:30
parent 2ed079830a
commit a4ef829046
38 changed files with 2068 additions and 1831 deletions

View File

@@ -19,6 +19,7 @@
"@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"argon2": "^0.30.3",
"aws-sdk": "^2.1364.0",
"axios": "^1.3.5",

View File

@@ -10,6 +10,7 @@
"@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4",
"argon2": "^0.30.3",
"aws-sdk": "^2.1364.0",
"axios": "^1.3.5",

View File

@@ -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,

View File

@@ -3,11 +3,31 @@ import {
ForcedSubject,
MongoAbility,
RawRuleOf,
buildMongoQueryMatcher,
createMongoAbility
} from "@casl/ability";
import { Membership } from "../models";
import { IRole } from "../models/role";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
const $glob: FieldInstruction<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const permissionSecretGlobPath = node.value;
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export enum ProjectPermissionActions {
Read = "read",
@@ -36,7 +56,7 @@ export enum ProjectPermissionSub {
type SubjectFields = {
environment: string;
secretPath?: string;
secretPath: string;
};
export type ProjectPermissionSet =
@@ -144,7 +164,7 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace);
return build();
return build({ conditionsMatcher });
};
export const adminProjectPermissions = buildAdminPermission();
@@ -180,7 +200,7 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build();
return build({ conditionsMatcher });
};
export const memberProjectPermissions = buildMemberPermission();
@@ -203,7 +223,7 @@ const buildViewerPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
return build();
return build({ conditionsMatcher });
};
export const viewerProjectPermission = buildViewerPermission();
@@ -228,7 +248,9 @@ export const getUserProjectPermissions = async (userId: string, workspaceId: str
if (membership.role === "viewer") return { permission: viewerProjectPermission, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions);
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
}

View File

@@ -3,6 +3,7 @@ import { MembershipOrg } from "../models";
import { IRole } from "../models/role";
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
import { ACCEPTED } from "../variables";
import { conditionsMatcher } from "./ProjectRoleService";
export enum OrgPermissionActions {
Read = "read",
@@ -74,7 +75,7 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
return build();
return build({ conditionsMatcher });
};
export const adminPermissions = buildAdminPermission();
@@ -92,7 +93,7 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
return build();
return build({ conditionsMatcher });
};
export const memberPermissions = buildMemberPermission();
@@ -119,7 +120,9 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
if (membership.role === "member") return { permission: memberPermissions, membership };
if (membership.role === "custom") {
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions);
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
conditionsMatcher
});
return { permission, membership };
}

View File

@@ -11,7 +11,9 @@ export const CreateRoleSchema = z.object({
.object({
subject: z.string().trim(),
action: z.string().trim(),
conditions: z.record(z.union([z.string().trim(), z.number()])).optional()
conditions: z
.record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })]))
.optional()
})
.array()
})
@@ -31,7 +33,9 @@ export const UpdateRoleSchema = z.object({
.object({
subject: z.string().trim(),
action: z.string().trim(),
conditions: z.record(z.union([z.string().trim(), z.number()])).optional()
conditions: z
.record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })]))
.optional()
})
.array()
.optional()

View File

@@ -189,7 +189,7 @@ export const BatchSecretsV2 = z.object({
workspaceId: z.string().trim(),
folderId: z.string().trim().default("root"),
environment: z.string().trim(),
secretPath: z.string().trim().optional(),
secretPath: z.string().trim().default("/"),
requests: z
.discriminatedUnion("method", [
z.object({
@@ -328,7 +328,7 @@ export const CreateSecretV3 = z.object({
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
metadata: z.record(z.string()).optional(),
metadata: z.record(z.string()).optional()
}),
params: z.object({
secretName: z.string().trim()

View File

@@ -41,6 +41,7 @@
"@stripe/stripe-js": "^1.46.0",
"@tanstack/react-query": "^4.23.0",
"@types/argon2-browser": "^1.18.1",
"@ucast/mongo2js": "^1.3.4",
"add": "^2.0.6",
"argon2-browser": "^1.18.0",
"axios": "^0.27.2",
@@ -65,6 +66,7 @@
"markdown-it": "^13.0.1",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
"posthog-js": "^1.58.0",
"query-string": "^7.1.3",
"react": "^17.0.2",
@@ -106,6 +108,7 @@
"@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4",
"@types/node": "^18.11.9",
"@types/picomatch": "^2.3.0",
"@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
@@ -8391,6 +8394,12 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"node_modules/@types/picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==",
"dev": true
},
"node_modules/@types/pretty-hrtime": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz",
@@ -29414,6 +29423,12 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"@types/picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==",
"dev": true
},
"@types/pretty-hrtime": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz",

View File

@@ -49,6 +49,7 @@
"@stripe/stripe-js": "^1.46.0",
"@tanstack/react-query": "^4.23.0",
"@types/argon2-browser": "^1.18.1",
"@ucast/mongo2js": "^1.3.4",
"add": "^2.0.6",
"argon2-browser": "^1.18.0",
"axios": "^0.27.2",
@@ -73,6 +74,7 @@
"markdown-it": "^13.0.1",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"picomatch": "^2.3.1",
"posthog-js": "^1.58.0",
"query-string": "^7.1.3",
"react": "^17.0.2",
@@ -114,6 +116,7 @@
"@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4",
"@types/node": "^18.11.9",
"@types/picomatch": "^2.3.0",
"@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",

View File

@@ -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<string, boolean>;
handleSelectTag: (wsTag: WsTag) => void;
handleTagOnMouseEnter: (wsTag: WsTag) => void;
handleTagOnMouseLeave: () => void;
checkIfTagIsVisible: (wsTag: WsTag) => boolean;
handleOnCreateTagOpen: () => void
wsTags: WsTag[] | undefined;
secKey: string;
selectedTagIds: Record<string, boolean>;
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 (
<PopoverContent
side="left"
className="relative max-h-96 w-auto min-w-[200px] p-2 overflow-y-auto overflow-x-hidden border border-mineshaft-600 bg-mineshaft-800 text-bunker-200"
hideCloseBtn
>
<div className=" text-center text-sm font-medium text-bunker-200">
Add tags to {secKey || "this secret"}
return (
<PopoverContent
side="left"
className="relative max-h-96 w-auto min-w-[200px] p-2 overflow-y-auto overflow-x-hidden border border-mineshaft-600 bg-mineshaft-800 text-bunker-200"
hideCloseBtn
>
<div className=" text-center text-sm font-medium text-bunker-200">
Add tags to {secKey || "this secret"}
</div>
<div className="absolute left-0 w-full border-mineshaft-600 border-t mt-2" />
<div className="flex flex-col space-y-1.5">
{wsTags?.map((wsTag: WsTag) => (
<div
key={`tag-${wsTag._id}`}
className="mt-4 h-[32px] relative flex items-center justify-start hover:border-mineshaft-600 hover:border hover:bg-mineshaft-700 p-2 rounded-md hover:text-bunker-200 bg-none"
onClick={() => handleSelectTag(wsTag)}
onMouseEnter={() => handleTagOnMouseEnter(wsTag)}
onMouseLeave={() => handleTagOnMouseLeave()}
tabIndex={0}
role="button"
onKeyDown={() => {}}
>
{(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && (
<Checkbox
id="autoCapitalization"
isChecked={selectedTagIds?.[wsTag.slug]}
className="absolute top-[50%] translate-y-[-50%] left-[10px] "
checkIndicatorBg={`${
!selectedTagIds?.[wsTag.slug] ? "text-transparent" : "text-mineshaft-800"
}`}
/>
)}
<div className="ml-7 flex items-center gap-3">
<div
className="w-[10px] h-[10px] rounded-full"
style={{ background: wsTag?.tagColor ? wsTag.tagColor : "#bec2c8" }}
>
{" "}
</div>
<span>{wsTag.slug}</span>
</div>
<div className="absolute left-0 w-full border-mineshaft-600 border-t mt-2" />
<div className="flex flex-col space-y-1.5">
{wsTags?.map((wsTag: WsTag) => (
<div key={`tag-${wsTag._id}`} className="mt-4 h-[32px] relative flex items-center justify-start hover:border-mineshaft-600 hover:border hover:bg-mineshaft-700 p-2 rounded-md hover:text-bunker-200 bg-none"
onClick={() => handleSelectTag(wsTag)}
onMouseEnter={() => handleTagOnMouseEnter(wsTag)}
onMouseLeave={() => handleTagOnMouseLeave()}
tabIndex={0} role="button"
onKeyDown={() => { }}>
{
</div>
))}
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
{(isAllowed) => (
<Button
onClick={() => handleOnCreateTagOpen()}
isDisabled={!isAllowed}
size="xs"
className="mt-2"
leftIcon={<FontAwesomeIcon icon={faPlus} className="ml-1 mr-2" />}
>
Add new tag
</Button>
)}
</ProjectPermissionCan>
</div>
</PopoverContent>
);
};
(checkIfTagIsVisible(wsTag) || selectedTagIds?.[wsTag.slug]) && <Checkbox
id="autoCapitalization"
isChecked={selectedTagIds?.[wsTag.slug]}
className="absolute top-[50%] translate-y-[-50%] left-[10px] "
checkIndicatorBg={`${!selectedTagIds?.[wsTag.slug] ? "text-transparent" : "text-mineshaft-800"}`}
/>
}
<div className="ml-7 flex items-center gap-3">
<div className="w-[10px] h-[10px] rounded-full" style={{ background: wsTag?.tagColor ? wsTag.tagColor : "#bec2c8" }}> </div>
<span >
{wsTag.slug}
</span>
</div>
</div>
))}
<div
className="h-[32px] relative flex items-center cursor-pointer justify-start border-mineshaft-600 border bg-mineshaft-700 p-2 rounded-md hover:text-bunker-200 bg-none"
onClick={() => handleOnCreateTagOpen()}
tabIndex={0} role="button"
onKeyDown={() => { }}>
<FontAwesomeIcon icon={faPlus} className="ml-1 mr-2" />
<span> Add new tag</span>
</div>
</div>
</PopoverContent>
)
}
export default AddTagPopoverContent
export default AddTagPopoverContent;

View File

@@ -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>
);
};

View File

@@ -11,7 +11,11 @@ type Props = {
// so when permission is allowed same tooltip will be reused to show helpertext
renderTooltip?: boolean;
allowedLabel?: string;
} & BoundCanProps<TProjectPermission>;
// BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission
// For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error
// tried a lot combinations
// REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15
} & BoundCanProps<any>;
export const ProjectPermissionCan: FunctionComponent<Props> = ({
label = "Permission Denied. Kindly contact your project admin",
@@ -22,7 +26,6 @@ export const ProjectPermissionCan: FunctionComponent<Props> = ({
...props
}) => {
const permission = useProjectPermission();
return (
<Can {...props} passThrough={passThrough} ability={props?.ability || permission}>
{(isAllowed, ability) => {

View File

@@ -1,2 +1,3 @@
export { OrgPermissionCan } from "./OrgPermissionCan";
export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
export { ProjectPermissionCan } from "./ProjectPermissionCan";

View File

@@ -27,7 +27,7 @@ export enum ProjectPermissionSub {
type SubjectFields = {
environment: string;
secretPath?: string;
secretPath: string;
};
export type ProjectPermissionSet =

View File

@@ -1,6 +1,8 @@
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { buildMongoQueryMatcher, createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, unpackRules } from "@casl/ability/extra";
import { useQuery } from "@tanstack/react-query";
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
import picomatch from "picomatch";
import { apiRequest } from "@app/config/request";
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
@@ -13,6 +15,29 @@ import {
TRole
} from "./types";
const $glob: FieldInstruction<string> = {
type: "field",
validate(instruction, value) {
if (typeof value !== "string") {
throw new Error(`"${instruction.name}" expects value to be a string`);
}
}
};
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const secretPath = context.get(object, node.field);
const permissionSecretGlobPath = node.value;
if (!secretPath) return false;
// console.log(
// secretPath,
// picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }),
// permissionSecretGlobPath
// );
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
};
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export const roleQueryKeys = {
getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const,
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
@@ -57,7 +82,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
enabled: Boolean(orgId),
select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(data);
const ability = createMongoAbility<OrgPermissionSet>(rule);
const ability = createMongoAbility<OrgPermissionSet>(rule, { conditionsMatcher });
return ability;
}
});

View File

@@ -739,16 +739,26 @@ export const AppLayout = ({ children }: LayoutProps) => {
<Controller
control={control}
name="addMembers"
defaultValue
defaultValue={false}
render={({ field: { onBlur, value, onChange } }) => (
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
<OrgPermissionCan
I={OrgPermissionActions.Read}
a={OrgPermissionSubjects.Member}
>
Add all members of my organization to this project
</Checkbox>
{(isAllowed) => (
<div>
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
isDisabled={!isAllowed}
onBlur={onBlur}
>
Add all members of my organization to this project
</Checkbox>
</div>
)}
</OrgPermissionCan>
)}
/>
</div>

View File

@@ -838,16 +838,26 @@ const OrganizationPage = withPermission(
<Controller
control={control}
name="addMembers"
defaultValue
defaultValue={false}
render={({ field: { onBlur, value, onChange } }) => (
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
onBlur={onBlur}
<OrgPermissionCan
I={OrgPermissionActions.Read}
a={OrgPermissionSubjects.Member}
>
Add all members of my organization to this project
</Checkbox>
{(isAllowed) => (
<div>
<Checkbox
id="add-project-layout"
isChecked={value}
onCheckedChange={onChange}
isDisabled={!isAllowed}
onBlur={onBlur}
>
Add all members of my organization to this project
</Checkbox>
</div>
)}
</OrgPermissionCan>
)}
/>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import { memo } from "react";
import { subject } from "@casl/ability";
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -9,6 +10,8 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
type Props = {
folders?: Array<{ id: string; name: string }>;
search?: string;
environment: string;
secretPath: string;
onFolderUpdate: (folderId: string, name: string) => void;
onFolderDelete: (folderId: string, name: string) => void;
onFolderOpen: (folderId: string) => void;
@@ -20,7 +23,9 @@ export const FolderSection = memo(
onFolderDelete: handleFolderDelete,
onFolderOpen: handleFolderOpen,
search = "",
folders = []
folders = [],
environment,
secretPath
}: Props) => {
return (
<>
@@ -51,7 +56,7 @@ export const FolderSection = memo(
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Folders}
a={subject(ProjectPermissionSub.Folders, { environment, secretPath })}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
@@ -72,7 +77,7 @@ export const FolderSection = memo(
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Folders}
a={subject(ProjectPermissionSub.Folders, { environment, secretPath })}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">

View File

@@ -1,4 +1,5 @@
import { useFormContext, useWatch } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -23,6 +24,8 @@ import { GenRandomNumber } from "./GenRandomNumber";
type Props = {
isDrawerOpen: boolean;
environment: string;
secretPath: string;
onOpenChange: (isOpen: boolean) => void;
index: number;
isReadOnly?: boolean;
@@ -41,7 +44,9 @@ export const SecretDetailDrawer = ({
isReadOnly,
onSecretDelete,
onSave,
onEnvCompare
onEnvCompare,
environment,
secretPath
}: Props): JSX.Element => {
const [canRevealSecVal, setCanRevealSecVal] = useToggle();
const [canRevealSecOverride, setCanRevealSecOverride] = useToggle();
@@ -89,7 +94,7 @@ export const SecretDetailDrawer = ({
<div className="flex w-full space-x-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly || !isAllowed}>
@@ -98,8 +103,8 @@ export const SecretDetailDrawer = ({
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Secrets}
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button

View File

@@ -1,6 +1,7 @@
import { ChangeEvent, DragEvent, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import {
faClone,
@@ -78,6 +79,8 @@ type Props = {
environments?: { name: string; slug: string }[];
workspaceId: string;
decryptFileKey: UserWsKeyPair;
environment: string;
secretPath: string;
};
export const SecretDropzone = ({
@@ -86,7 +89,9 @@ export const SecretDropzone = ({
onAddNewSecret,
environments = [],
workspaceId,
decryptFileKey
decryptFileKey,
environment,
secretPath
}: Props): JSX.Element => {
const { t } = useTranslation();
const [isDragActive, setDragActive] = useToggle();
@@ -109,16 +114,16 @@ export const SecretDropzone = ({
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
});
const secretPath = watch("secretPath");
const envCopySecPath = watch("secretPath");
const selectedEnvSlug = watch("environment");
const debouncedSecretPath = useDebounce(secretPath);
const debouncedEnvCopySecretPath = useDebounce(envCopySecPath);
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
env: selectedEnvSlug,
secretPath: debouncedSecretPath,
secretPath: debouncedEnvCopySecretPath,
isPaused:
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) &&
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedEnvCopySecretPath)) &&
!popUp.importSecEnv.isOpen,
decryptFileKey
});
@@ -126,7 +131,7 @@ export const SecretDropzone = ({
useEffect(() => {
setValue("secrets", {});
setSearchFilter("");
}, [debouncedSecretPath]);
}, [debouncedEnvCopySecretPath]);
const handleDrag = (e: DragEvent) => {
e.preventDefault();
@@ -238,7 +243,7 @@ export const SecretDropzone = ({
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<input
@@ -271,16 +276,22 @@ export const SecretDropzone = ({
}}
>
<ModalTrigger asChild>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<Button isDisabled={!isAllowed} variant="star" size={isSmaller ? "xs" : "sm"}>
Copy Secrets From An Environment
</Button>
)}
</ProjectPermissionCan>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
variant="star"
size={isSmaller ? "xs" : "sm"}
>
Copy Secrets From An Environment
</Button>
)}
</ProjectPermissionCan>
</div>
</ModalTrigger>
<ModalContent
className="max-w-2xl"
@@ -416,7 +427,7 @@ export const SecretDropzone = ({
{!isSmaller && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button variant="star" onClick={onAddNewSecret} isDisabled={!isAllowed}>

View File

@@ -1,4 +1,5 @@
import { useEffect } from "react";
import { subject } from "@casl/ability";
import { useSortable } from "@dnd-kit/sortable";
import {
faFileImport,
@@ -11,11 +12,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub,useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
type Props = {
onDelete: (environment: string, secretPath: string) => void;
environment: string;
secretPath: string;
importedEnv: string;
importedSecPath: string;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
@@ -40,7 +43,9 @@ export const SecretImportItem = ({
importedSecPath,
onDelete,
importedSecrets = [],
searchTerm = ""
searchTerm = "",
secretPath,
environment
}: Props) => {
const [isExpanded, setIsExpanded] = useToggle();
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
@@ -114,7 +119,7 @@ export const SecretImportItem = ({
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SecretImports}
a={subject(ProjectPermissionSub.SecretImports, { environment, secretPath })}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">

View File

@@ -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}

View File

@@ -8,6 +8,7 @@ import {
UseFormSetValue,
useWatch
} from "react-hook-form";
import { subject } from "@casl/ability";
import {
faCheck,
faCodeBranch,
@@ -22,31 +23,34 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { cx } from "cva";
import { twMerge } from "tailwind-merge";
// TODO:(akhilmhdh): Refactor this
import AddTagPopoverContent from "@app/components/AddTagPopoverContent/AddTagPopoverContent";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
FormControl,
HoverCard,
HoverCardContent,
HoverCardTrigger,
IconButton,
Input,
Popover,
PopoverContent,
PopoverTrigger,
SecretInput,
Tag,
TextArea,
Tooltip
} from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { WsTag } from "@app/hooks/api/types";
import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent";
import { FormData, SecretActionType } from "../../DashboardPage.utils";
type Props = {
index: number;
environment: string;
secretPath: string;
// backend generated unique id
secUniqId?: string;
// permission and external state's that decided to hide or show
@@ -74,6 +78,8 @@ type Props = {
export const SecretInputRow = memo(
({
index,
secretPath,
environment,
isSecretValueHidden,
onRowExpand,
isReadOnly,
@@ -84,7 +90,7 @@ export const SecretInputRow = memo(
onSecretDelete,
searchTerm,
control,
// register,
register,
setValue,
isKeyError,
keyError,
@@ -222,7 +228,6 @@ export const SecretInputRow = memo(
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
</td>
<Controller
control={control}
defaultValue=""
@@ -361,16 +366,24 @@ export const SecretInputRow = memo(
<Popover>
<PopoverTrigger asChild>
<div className="w-0 group-hover:w-6 data-[state=open]:w-6">
<Tooltip content="Add tags">
<IconButton
variant="plain"
size="md"
ariaLabel="add-tag"
className="py-[0.42rem]"
>
<FontAwesomeIcon icon={faTags} />
</IconButton>
</Tooltip>
<ProjectPermissionCan
renderTooltip
allowedLabel="Add Tags"
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<IconButton
isDisabled={!isAllowed}
variant="plain"
size="md"
ariaLabel="add-tags"
className="py-[0.42rem]"
>
<FontAwesomeIcon icon={faTags} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
</PopoverTrigger>
<AddTagPopoverContent
@@ -390,55 +403,76 @@ export const SecretInputRow = memo(
<div className="flex h-8 flex-row items-center pr-2">
{!isAddOnly && (
<div>
<Tooltip content="Override with a personal value">
<IconButton
variant="plain"
className={twMerge(
"mt-0.5 w-0 overflow-hidden p-0 group-hover:ml-1 group-hover:w-7",
isOverridden && "ml-1 w-7 text-primary"
)}
onClick={onSecretOverride}
size="md"
isDisabled={isRollbackMode || isReadOnly}
ariaLabel="info"
>
<div className="flex items-center space-x-1">
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
</div>
</IconButton>
</Tooltip>
<ProjectPermissionCan
renderTooltip
allowedLabel="Override with a personal value"
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<IconButton
variant="plain"
className={twMerge(
"mt-0.5 w-0 overflow-hidden p-0 group-hover:ml-1 group-hover:w-7",
isOverridden && "ml-1 w-7 text-primary"
)}
onClick={onSecretOverride}
size="md"
isDisabled={isRollbackMode || isReadOnly || !isAllowed}
ariaLabel="info"
>
<div className="flex items-center space-x-1">
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
</div>
</IconButton>
)}
</ProjectPermissionCan>
</div>
)}
<Tooltip content="Comment">
<div className="mt-0.5 overflow-hidden ">
<Popover>
<PopoverTrigger asChild>
<IconButton
className={twMerge(
"w-7 overflow-hidden p-0",
"w-0 group-hover:w-7 data-[state=open]:w-7",
hasComment ? "w-7 text-primary" : "group-hover:w-7"
)}
variant="plain"
size="md"
ariaLabel="add-tag"
<div className="mt-0.5 overflow-hidden ">
<Popover>
<PopoverTrigger asChild>
<div>
<ProjectPermissionCan
renderTooltip
allowedLabel="Comment"
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
<FontAwesomeIcon icon={faComment} />
</IconButton>
</PopoverTrigger>
<AddTagPopoverContent
wsTags={wsTags}
secKey={secKey || "this secret"}
selectedTagIds={selectedTagIds}
handleSelectTag={(wsTag: WsTag) => onSelectTag(wsTag)}
handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)}
handleTagOnMouseLeave={() => handleTagOnMouseLeave()}
checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)}
handleOnCreateTagOpen={() => onCreateTagOpen()}
/>
</Popover>
</div>
</Tooltip>
{(isAllowed) => (
<IconButton
className={twMerge(
"w-7 overflow-hidden p-0",
"w-0 group-hover:w-7 data-[state=open]:w-7",
hasComment ? "w-7 text-primary" : "group-hover:w-7"
)}
isDisabled={!isAllowed}
variant="plain"
size="md"
ariaLabel="add-comment"
>
<FontAwesomeIcon icon={faComment} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
</PopoverTrigger>
<PopoverContent
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
sticky="always"
>
<FormControl label="Comment" className="mb-0">
<TextArea
isDisabled={isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly}
className="border border-mineshaft-600 text-sm"
{...register(`secrets.${index}.comment`)}
rows={8}
cols={30}
/>
</FormControl>
</PopoverContent>
</Popover>
</div>
</div>
<div className="duration-0 flex w-16 justify-center overflow-hidden border-l border-mineshaft-600 pl-2 transition-all">
<div className="flex h-8 items-center space-x-2.5">
@@ -457,29 +491,29 @@ export const SecretInputRow = memo(
</Tooltip>
</div>
)}
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Secrets}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete">
<IconButton
size="lg"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
onClick={() => {
onSecretDelete(index, secKey, secId, idOverride);
}}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
<div className="opacity-0 group-hover:opacity-100">
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
renderTooltip
allowedLabel="Delete"
>
{(isAllowed) => (
<IconButton
size="lg"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
onClick={() => {
onSecretDelete(index, secKey, secId, idOverride);
}}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</ProjectPermissionCan>
</div>
</div>
</div>
</td>

View File

@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
import {
Checkbox,
FormControl,
Input,
Select,
SelectItem,
@@ -68,7 +69,7 @@ export const MultiEnvProjectPermission = ({
const handlePermissionChange = (val: Permission) => {
switch (val) {
case Permission.NoAccess:
setValue(`permissions.${formName}`, {}, { shouldDirty: true });
setValue(`permissions.${formName}`, undefined, { shouldDirty: true });
break;
case Permission.FullAccess:
setValue(
@@ -151,7 +152,14 @@ export const MultiEnvProjectPermission = ({
name={`permissions.${formName}.${slug}.secretPath`}
control={control}
render={({ field }) => (
<Input {...field} className="w-full overflow-ellipsis" />
/* eslint-disable-next-line no-template-curly-in-string */
<FormControl helperText="Ex pattern: /, /**, /{folder1,folder2}">
<Input
{...field}
className="w-full overflow-ellipsis"
placeholder="Glob patterns are supported"
/>
</FormControl>
)}
/>
</Td>

View File

@@ -133,6 +133,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
const handleRoleUpdate = async (el: TFormSchema) => {
if (!role?._id) return;
console.log(el);
try {
await updateRole({
orgId,

View File

@@ -73,7 +73,7 @@ const multiEnvApi2Form = (
}
const secretEnv = permission?.conditions?.environment || "all";
const secretPath = permission?.conditions?.secretPath;
const secretPath = permission?.conditions?.secretPath?.$glob;
// initialize
if (formVal && !formVal?.[secretEnv]) {
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
@@ -109,6 +109,8 @@ const multiEnvForm2Api = (
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
subject: (typeof MULTI_ENV_KEY)[number]
) => {
if (!formVal) return;
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
// if any of them is set in all push it without any condition
PERMISSION_ACTIONS.forEach((action) => {
@@ -130,7 +132,8 @@ const multiEnvForm2Api = (
// if not full access for an action
if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) {
const conditions: Record<string, unknown> = { environment: slug };
if (formVal[slug]?.secretPath) conditions.secretPath = formVal?.[slug]?.secretPath;
if (formVal[slug]?.secretPath)
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
permissions.push({ action, subject, conditions });
}
@@ -141,17 +144,22 @@ const multiEnvForm2Api = (
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
const permissions: TProjectPermission[] = [];
MULTI_ENV_KEY.forEach((formName) => {
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(formVal?.[formName] || {})), formName);
});
// other than workspace everything else follows same
// if in future there is a different follow the above on how workspace is done
Object.entries(formVal || {}).forEach(([rule, actions]) => {
Object.entries(actions).forEach(([action, isAllowed]) => {
if (isAllowed) {
permissions.push({ subject: rule, action });
}
});
if (MULTI_ENV_KEY.includes(rule as (typeof MULTI_ENV_KEY)[number])) {
multiEnvForm2Api(
permissions,
JSON.parse(JSON.stringify(actions || {})),
rule as (typeof MULTI_ENV_KEY)[number]
);
} else {
Object.entries(actions).forEach(([action, isAllowed]) => {
if (isAllowed) {
permissions.push({ subject: rule, action });
}
});
}
});
return permissions;
};

View File

@@ -13,6 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { PermissionDeniedBanner } from "@app/components/permissions";
import {
Button,
EmptyState,
@@ -36,7 +37,6 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { withProjectPermission } from "@app/hoc";
import {
useCreateSecretV3,
useDeleteSecretV3,
@@ -50,7 +50,7 @@ import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
const SecretOverview = () => {
export const SecretOverviewPage = () => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
@@ -91,11 +91,20 @@ const SecretOverview = () => {
}, [isWorkspaceLoading, workspaceId, router.isReady]);
const userAvailableEnvs =
currentWorkspace?.environments?.filter(({ slug }) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath: secretPath || "/" })
)
currentWorkspace?.environments?.filter(
({ slug }) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath })
) ||
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Folders, { environment: slug, secretPath })
) ||
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment: slug, secretPath })
)
) || [];
const {
@@ -371,18 +380,23 @@ const SecretOverview = () => {
onClick={handleFolderClick}
/>
))}
{filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
key={`overview-${key}-${index + 1}`}
environments={userAvailableEnvs}
secretKey={key}
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
/>
))}
{userAvailableEnvs?.length > 0 ? (
filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
secretPath={secretPath}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
key={`overview-${key}-${index + 1}`}
environments={userAvailableEnvs}
secretKey={key}
getSecretByKey={getSecretByKey}
expandableColWidth={expandableTableWidth}
/>
))
) : (
<PermissionDeniedBanner />
)}
</TBody>
<TFoot>
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
@@ -414,8 +428,3 @@ const SecretOverview = () => {
</div>
);
};
export const SecretOverviewPage = withProjectPermission(SecretOverview, {
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Secrets
});

View File

@@ -1,4 +1,5 @@
import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -14,6 +15,7 @@ type Props = {
isCreatable?: boolean;
isVisible?: boolean;
environment: string;
secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretDelete: (env: string, key: string) => Promise<void>;
@@ -27,6 +29,7 @@ export const SecretEditRow = ({
onSecretCreate,
onSecretDelete,
environment,
secretPath,
isVisible
}: Props) => {
const {
@@ -95,7 +98,7 @@ export const SecretEditRow = ({
<>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Secrets}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<div>

View File

@@ -18,6 +18,7 @@ import { SecretEditRow } from "./SecretEditRow";
type Props = {
secretKey: string;
secretPath: string;
environments: { name: string; slug: string }[];
expandableColWidth: number;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
@@ -29,6 +30,7 @@ type Props = {
export const SecretOverviewTableRow = ({
secretKey,
environments = [],
secretPath,
getSecretByKey,
onSecretUpdate,
onSecretCreate,
@@ -73,9 +75,11 @@ export const SecretOverviewTableRow = ({
>
<div className="h-full w-full border-r border-mineshaft-600 py-[0.85rem] px-5">
<div className="flex justify-center">
{!isSecretEmpty && <Tooltip content={isSecretPresent ? "Present secret" : "Missing secret"}>
<FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />
</Tooltip>}
{!isSecretEmpty && (
<Tooltip content={isSecretPresent ? "Present secret" : "Missing secret"}>
<FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />
</Tooltip>
)}
{isSecretEmpty && (
<Tooltip content="Empty value">
<FontAwesomeIcon icon={faCircle} />
@@ -141,6 +145,7 @@ export const SecretOverviewTableRow = ({
</td>
<td className="col-span-2 h-8 w-full">
<SecretEditRow
secretPath={secretPath}
isVisible={isSecretVisible}
secretName={secretKey}
defaultValue={secret?.value}

View File

@@ -2,53 +2,49 @@ import { useTranslation } from "react-i18next";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { OrgPermissionCan, PermissionDeniedBanner } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { usePopUp } from "@app/hooks";
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
export const OrgIncidentContactsSection = withPermission(
() => {
const { t } = useTranslation();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact"
] as const);
export const OrgIncidentContactsSection = () => {
const { t } = useTranslation();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact"
] as const);
const permission = useOrgPermission();
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-4">
<p className="min-w-max text-xl font-semibold">
{t("section.incident.incident-contacts")}
</p>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.IncidentAccount}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addContact")}
>
Add contact
</Button>
)}
</OrgPermissionCan>
</div>
<OrgIncidentContactsTable />
<AddOrgIncidentContactModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-4">
<p className="min-w-max text-xl font-semibold">{t("section.incident.incident-contacts")}</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addContact")}
>
Add contact
</Button>
)}
</OrgPermissionCan>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.IncidentAccount }
);
{permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount) ? (
<OrgIncidentContactsTable />
) : (
<PermissionDeniedBanner />
)}
<AddOrgIncidentContactModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
</div>
);
};

View File

@@ -7,7 +7,6 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { useRenameOrg } from "@app/hooks/api";
const formSchema = yup.object({
@@ -16,77 +15,70 @@ const formSchema = yup.object({
type FormData = yup.InferType<typeof formSchema>;
export const OrgNameChangeSection = withPermission(
(): JSX.Element => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { handleSubmit, control, reset } = useForm<FormData>({
resolver: yupResolver(formSchema)
});
const { mutateAsync, isLoading } = useRenameOrg();
export const OrgNameChangeSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { handleSubmit, control, reset } = useForm<FormData>({
resolver: yupResolver(formSchema)
});
const { mutateAsync, isLoading } = useRenameOrg();
useEffect(() => {
if (currentOrg) {
reset({ name: currentOrg.name });
}
}, [currentOrg]);
useEffect(() => {
if (currentOrg) {
reset({ name: currentOrg.name });
}
}, [currentOrg]);
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!currentOrg?._id) return;
if (name === "") return;
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!currentOrg?._id) return;
if (name === "") return;
await mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
createNotification({
text: "Successfully renamed organization",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to rename organization",
type: "error"
});
}
};
await mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
createNotification({
text: "Successfully renamed organization",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to rename organization",
type: "error"
});
}
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder="Acme Corp" {...field} />
</FormControl>
)}
control={control}
name="name"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
isLoading={isLoading}
isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
>
Save
</Button>
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder="Acme Corp" {...field} />
</FormControl>
)}
</OrgPermissionCan>
</form>
);
},
{
action: OrgPermissionActions.Read,
subject: OrgPermissionSubjects.Settings,
containerClassName: "mb-4"
}
);
control={control}
name="name"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
isLoading={isLoading}
isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
>
Save
</Button>
)}
</OrgPermissionCan>
</form>
);
};

View File

@@ -3,8 +3,6 @@ import { useTranslation } from "react-i18next";
import { Tab } from "@headlessui/react";
import NavHeader from "@app/components/navigation/NavHeader";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
@@ -16,50 +14,47 @@ const tabs = [
{ name: "Webhooks", key: "tab-project-webhooks" }
];
export const ProjectSettingsPage = withProjectPermission(
() => {
const { t } = useTranslation();
return (
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
<div className="w-full max-w-screen-lg">
<div className="relative right-5 ml-4">
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
</div>
<div className="my-8">
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tab.Group>
<Tab.List className="mb-4 w-full border-b-2 border-mineshaft-800">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<ProjectGeneralTab />
</Tab.Panel>
<Tab.Panel>
<ProjectServiceTokensTab />
</Tab.Panel>
<Tab.Panel>
<WebhooksTab />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
export const ProjectSettingsPage = () => {
const { t } = useTranslation();
return (
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
<div className="w-full max-w-screen-lg">
<div className="relative right-5 ml-4">
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
</div>
<div className="my-8">
<p className="text-3xl font-semibold text-gray-200">{t("settings.project.title")}</p>
</div>
<Tab.Group>
<Tab.List className="mb-4 w-full border-b-2 border-mineshaft-800">
{tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}>
{({ selected }) => (
<button
type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<ProjectGeneralTab />
</Tab.Panel>
<Tab.Panel>
<ProjectServiceTokensTab />
</Tab.Panel>
<Tab.Panel>
<WebhooksTab />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
);
</div>
);
};

View File

@@ -4,61 +4,57 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
import { ProjectPermissionCan } from "@app/components/permissions";
import { Checkbox } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useToggleAutoCapitalization } from "@app/hooks/api";
export const AutoCapitalizationSection = withProjectPermission(
() => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const { mutateAsync } = useToggleAutoCapitalization();
export const AutoCapitalizationSection = () => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const { mutateAsync } = useToggleAutoCapitalization();
const handleToggleCapitalizationToggle = async (state: boolean) => {
try {
if (!currentWorkspace?._id) return;
const handleToggleCapitalizationToggle = async (state: boolean) => {
try {
if (!currentWorkspace?._id) return;
await mutateAsync({
workspaceID: currentWorkspace._id,
state
});
await mutateAsync({
workspaceID: currentWorkspace._id,
state
});
const text = `Successfully ${state ? "enabled" : "disabled"} auto capitalization`;
createNotification({
text,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update auto capitalization",
type: "error"
});
}
};
const text = `Successfully ${state ? "enabled" : "disabled"} auto capitalization`;
createNotification({
text,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update auto capitalization",
type: "error"
});
}
};
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isDisabled={!isAllowed}
isChecked={currentWorkspace?.autoCapitalization ?? false}
onCheckedChange={(state) => {
handleToggleCapitalizationToggle(state as boolean);
}}
>
{t("settings.project.auto-capitalization-description")}
</Checkbox>
</div>
)}
</ProjectPermissionCan>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
);
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isDisabled={!isAllowed}
isChecked={currentWorkspace?.autoCapitalization ?? false}
onCheckedChange={(state) => {
handleToggleCapitalizationToggle(state as boolean);
}}
>
{t("settings.project.auto-capitalization-description")}
</Checkbox>
</div>
)}
</ProjectPermissionCan>
</div>
);
};

View File

@@ -5,113 +5,106 @@ import {
} from "@app/components/utilities/cryptography/crypto";
import { Checkbox } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
export const E2EESection = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
export const E2EESection = () => {
const { currentWorkspace } = useWorkspace();
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
/**
* Activate bot for project by performing the following steps:
* 1. Get the (encrypted) project key
* 2. Decrypt project key with user's private key
* 3. Encrypt project key with bot's public key
* 4. Send encrypted project key to backend and set bot status to active
*/
const toggleBotActivate = async () => {
let botKey;
try {
if (!currentWorkspace?._id) return;
/**
* Activate bot for project by performing the following steps:
* 1. Get the (encrypted) project key
* 2. Decrypt project key with user's private key
* 3. Encrypt project key with bot's public key
* 4. Send encrypted project key to backend and set bot status to active
*/
const toggleBotActivate = async () => {
let botKey;
try {
if (!currentWorkspace?._id) return;
if (bot && wsKey) {
// case: there is a bot
if (bot && wsKey) {
// case: there is a bot
if (!bot.isActive) {
// bot is not active -> activate bot
if (!bot.isActive) {
// bot is not active -> activate bot
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: bot.publicKey,
privateKey: PRIVATE_KEY
});
botKey = {
encryptedKey: ciphertext,
nonce
};
await updateBotActiveStatus({
workspaceId: currentWorkspace._id,
botKey,
isActive: true,
botId: bot._id
});
} else {
// bot is active -> deactivate bot
await updateBotActiveStatus({
isActive: false,
botId: bot._id,
workspaceId: currentWorkspace._id
});
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
}
} catch (err) {
console.error(err);
}
};
return bot ? (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
<p className="text-gray-400 mb-8">
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
read/decrypt your secret values.
</p>
<p className="text-gray-400 mb-8">
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
</div>
)}
</ProjectPermissionCan>
</div>
) : (
<div />
);
},
{
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Settings
}
);
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: bot.publicKey,
privateKey: PRIVATE_KEY
});
botKey = {
encryptedKey: ciphertext,
nonce
};
await updateBotActiveStatus({
workspaceId: currentWorkspace._id,
botKey,
isActive: true,
botId: bot._id
});
} else {
// bot is active -> deactivate bot
await updateBotActiveStatus({
isActive: false,
botId: bot._id,
workspaceId: currentWorkspace._id
});
}
}
} catch (err) {
console.error(err);
}
};
return bot ? (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
<p className="text-gray-400 mb-8">
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
read/decrypt your secret values.
</p>
<p className="text-gray-400 mb-8">
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
</div>
)}
</ProjectPermissionCan>
</div>
) : (
<div />
);
};

View File

@@ -2,15 +2,15 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { useDeleteWsEnvironment } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -18,111 +18,113 @@ import { AddEnvironmentModal } from "./AddEnvironmentModal";
import { EnvironmentTable } from "./EnvironmentTable";
import { UpdateEnvironmentModal } from "./UpdateEnvironmentModal";
export const EnvironmentSection = withProjectPermission(
() => {
const { createNotification } = useNotificationContext();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
export const EnvironmentSection = () => {
const { createNotification } = useNotificationContext();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
const permision = useProjectPermission();
const deleteWsEnvironment = useDeleteWsEnvironment();
const deleteWsEnvironment = useDeleteWsEnvironment();
const isMoreEnvironmentsAllowed =
subscription?.environmentLimit && currentWorkspace?.environments
? currentWorkspace.environments.length < subscription.environmentLimit
: true;
const isMoreEnvironmentsAllowed =
subscription?.environmentLimit && currentWorkspace?.environments
? currentWorkspace.environments.length < subscription.environmentLimit
: true;
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"createEnv",
"updateEnv",
"deleteEnv",
"upgradePlan"
] as const);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"createEnv",
"updateEnv",
"deleteEnv",
"upgradePlan"
] as const);
const onEnvDeleteSubmit = async (environmentSlug: string) => {
try {
if (!currentWorkspace?._id) return;
const onEnvDeleteSubmit = async (environmentSlug: string) => {
try {
if (!currentWorkspace?._id) return;
await deleteWsEnvironment.mutateAsync({
workspaceID: currentWorkspace._id,
environmentSlug
});
await deleteWsEnvironment.mutateAsync({
workspaceID: currentWorkspace._id,
environmentSlug
});
createNotification({
text: "Successfully deleted environment",
type: "success"
});
createNotification({
text: "Successfully deleted environment",
type: "success"
});
handlePopUpClose("deleteEnv");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete environment",
type: "error"
});
}
};
handlePopUpClose("deleteEnv");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete environment",
type: "error"
});
}
};
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Environments}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isMoreEnvironmentsAllowed) {
handlePopUpOpen("createEnv");
} else {
handlePopUpOpen("upgradePlan");
}
}}
isDisabled={!isAllowed}
>
Create environment
</Button>
)}
</ProjectPermissionCan>
</div>
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Environments}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isMoreEnvironmentsAllowed) {
handlePopUpOpen("createEnv");
} else {
handlePopUpOpen("upgradePlan");
}
}}
isDisabled={!isAllowed}
>
Create environment
</Button>
)}
</ProjectPermissionCan>
</div>
<p className="text-gray-400 mb-8">
Choose which environments will show up in your dashboard like development, staging,
production
</p>
<EnvironmentTable handlePopUpOpen={handlePopUpOpen} />
<AddEnvironmentModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<UpdateEnvironmentModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteEnv.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteEnv?.data as { name: string })?.name || " "
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteEnv", isOpen)}
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ""}
onDeleteApproved={() =>
onEnvDeleteSubmit((popUp?.deleteEnv?.data as { slug: string })?.slug)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan."
/>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Environments }
);
<p className="text-gray-400 mb-8">
Choose which environments will show up in your dashboard like development, staging,
production
</p>
{permision.can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments) ? (
<EnvironmentTable handlePopUpOpen={handlePopUpOpen} />
) : (
<PermissionDeniedBanner />
)}
<AddEnvironmentModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<UpdateEnvironmentModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteEnv.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteEnv?.data as { name: string })?.name || " "
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteEnv", isOpen)}
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ""}
onDeleteApproved={() =>
onEnvDeleteSubmit((popUp?.deleteEnv?.data as { slug: string })?.slug)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan."
/>
</div>
);
};

View File

@@ -5,7 +5,6 @@ import {
} from "@app/components/utilities/cryptography/crypto";
import { Button } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import {
useGetUserWsKey,
useGetWorkspaceIndexStatus,
@@ -16,73 +15,70 @@ import {
// TODO: add check so that this only shows up if user is
// an admin in the workspace
export const ProjectIndexSecretsSection = withProjectPermission(
() => {
const { currentWorkspace } = useWorkspace();
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
currentWorkspace?._id ?? ""
);
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
export const ProjectIndexSecretsSection = () => {
const { currentWorkspace } = useWorkspace();
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
currentWorkspace?._id ?? ""
);
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
const onEnableBlindIndices = async () => {
if (!currentWorkspace?._id) return;
if (!encryptedSecrets) return;
if (!latestFileKey) return;
const onEnableBlindIndices = async () => {
if (!currentWorkspace?._id) return;
if (!encryptedSecrets) return;
if (!latestFileKey) return;
const key = decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
const key = decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
const secretName = decryptSymmetric({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag,
key
});
const secretsToUpdate = encryptedSecrets.map((encryptedSecret) => {
const secretName = decryptSymmetric({
ciphertext: encryptedSecret.secretKeyCiphertext,
iv: encryptedSecret.secretKeyIV,
tag: encryptedSecret.secretKeyTag,
key
});
return {
secretName,
_id: encryptedSecret._id
};
});
return {
secretName,
_id: encryptedSecret._id
};
});
await nameWorkspaceSecrets.mutateAsync({
workspaceId: currentWorkspace._id,
secretsToUpdate
});
};
await nameWorkspaceSecrets.mutateAsync({
workspaceId: currentWorkspace._id,
secretsToUpdate
});
};
return !isBlindIndexedLoading && !isBlindIndexed ? (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
<p className="text-gray-400 mb-8">
Your project, created before the introduction of blind indexing, contains unindexed
secrets. To access individual secrets by name through the SDK and public API, please
enable blind indexing.
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Button
onClick={onEnableBlindIndices}
isDisabled={!isAllowed}
color="mineshaft"
size="sm"
type="submit"
>
Enable Blind Indexing
</Button>
)}
</ProjectPermissionCan>
</div>
) : (
<div />
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
);
return !isBlindIndexedLoading && !isBlindIndexed ? (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
<p className="text-gray-400 mb-8">
Your project, created before the introduction of blind indexing, contains unindexed secrets.
To access individual secrets by name through the SDK and public API, please enable blind
indexing.
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<Button
onClick={onEnableBlindIndices}
isDisabled={!isAllowed}
color="mineshaft"
size="sm"
type="submit"
>
Enable Blind Indexing
</Button>
)}
</ProjectPermissionCan>
</div>
) : (
<div />
);
};

View File

@@ -2,10 +2,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteWsTag } from "@app/hooks/api";
@@ -14,80 +13,82 @@ import { SecretTagsTable } from "./SecretTagsTable";
type DeleteModalData = { name: string; id: string };
export const SecretTagsSection = withProjectPermission(
(): JSX.Element => {
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"CreateSecretTag",
"deleteTagConfirmation"
] as const);
export const SecretTagsSection = (): JSX.Element => {
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
"CreateSecretTag",
"deleteTagConfirmation"
] as const);
const permission = useProjectPermission();
const deleteWsTag = useDeleteWsTag();
const deleteWsTag = useDeleteWsTag();
const onDeleteApproved = async () => {
try {
await deleteWsTag.mutateAsync({
tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id
});
const onDeleteApproved = async () => {
try {
await deleteWsTag.mutateAsync({
tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id
});
createNotification({
text: "Successfully deleted tag",
type: "success"
});
createNotification({
text: "Successfully deleted tag",
type: "success"
});
handlePopUpClose("deleteTagConfirmation");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete the tag",
type: "error"
});
}
};
handlePopUpClose("deleteTagConfirmation");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete the tag",
type: "error"
});
}
};
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
console.log("x");
handlePopUpOpen("CreateSecretTag");
console.log("x2");
}}
isDisabled={!isAllowed}
>
Create tag
</Button>
)}
</ProjectPermissionCan>
</div>
<p className="text-gray-400 mb-8">
Every secret can be assigned to one or more tags. Here you can add and remove tags for the
current project.
</p>
<SecretTagsTable handlePopUpOpen={handlePopUpOpen} />
<AddSecretTagModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteTagConfirmation.isOpen}
title={`Delete ${
(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name || " "
} api key?`}
onChange={(isOpen) => handlePopUpToggle("deleteTagConfirmation", isOpen)}
deleteKey={(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose("deleteTagConfirmation")}
onDeleteApproved={onDeleteApproved}
/>
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
console.log("x");
handlePopUpOpen("CreateSecretTag");
console.log("x2");
}}
isDisabled={!isAllowed}
>
Create tag
</Button>
)}
</ProjectPermissionCan>
</div>
);
},
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Tags }
);
<p className="text-gray-400 mb-8">
Every secret can be assigned to one or more tags. Here you can add and remove tags for the
current project.
</p>
{permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? (
<SecretTagsTable handlePopUpOpen={handlePopUpOpen} />
) : (
<PermissionDeniedBanner />
)}
<AddSecretTagModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteTagConfirmation.isOpen}
title={`Delete ${
(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name || " "
} api key?`}
onChange={(isOpen) => handlePopUpToggle("deleteTagConfirmation", isOpen)}
deleteKey={(popUp?.deleteTagConfirmation?.data as DeleteModalData)?.name}
onClose={() => handlePopUpClose("deleteTagConfirmation")}
onDeleteApproved={onDeleteApproved}
/>
</div>
);
};