mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 13:27:22 +00:00
feat(rbac): added glob support in permission and revealed settings
This commit is contained in:
Generated
+1
@@ -19,6 +19,7 @@
|
|||||||
"@sentry/tracing": "^7.48.0",
|
"@sentry/tracing": "^7.48.0",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/libsodium-wrappers": "^0.7.10",
|
"@types/libsodium-wrappers": "^0.7.10",
|
||||||
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"aws-sdk": "^2.1364.0",
|
"aws-sdk": "^2.1364.0",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"@sentry/tracing": "^7.48.0",
|
"@sentry/tracing": "^7.48.0",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/libsodium-wrappers": "^0.7.10",
|
"@types/libsodium-wrappers": "^0.7.10",
|
||||||
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"aws-sdk": "^2.1364.0",
|
"aws-sdk": "^2.1364.0",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
import { validateRequest } from "../../../helpers/validation";
|
||||||
import { Secret } from "../../../models";
|
import { Folder, Secret } from "../../../models";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
@@ -11,6 +11,7 @@ import { BadRequestError } from "../../../utils/errors";
|
|||||||
import * as reqValidator from "../../../validation";
|
import * as reqValidator from "../../../validation";
|
||||||
import { SecretVersion } from "../../models";
|
import { SecretVersion } from "../../models";
|
||||||
import { EESecretService } from "../../services";
|
import { EESecretService } from "../../services";
|
||||||
|
import { getFolderWithPathFromId } from "../../../services/FolderService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret versions for secret with id [secretId]
|
* Return secret versions for secret with id [secretId]
|
||||||
@@ -164,10 +165,6 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Create,
|
||||||
ProjectPermissionSub.SecretRollback
|
ProjectPermissionSub.SecretRollback
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment })
|
|
||||||
);
|
|
||||||
|
|
||||||
// validate secret version
|
// validate secret version
|
||||||
const oldSecretVersion = await SecretVersion.findOne({
|
const oldSecretVersion = await SecretVersion.findOne({
|
||||||
@@ -194,6 +191,15 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
keyEncoding
|
keyEncoding
|
||||||
} = oldSecretVersion;
|
} = oldSecretVersion;
|
||||||
|
|
||||||
|
let secretPath = "/";
|
||||||
|
const folders = await Folder.findOne({ workspace, environment });
|
||||||
|
if (folders)
|
||||||
|
secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/";
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath })
|
||||||
|
);
|
||||||
|
|
||||||
// update secret
|
// update secret
|
||||||
const secret = await Secret.findByIdAndUpdate(
|
const secret = await Secret.findByIdAndUpdate(
|
||||||
secretId,
|
secretId,
|
||||||
|
|||||||
@@ -3,11 +3,31 @@ import {
|
|||||||
ForcedSubject,
|
ForcedSubject,
|
||||||
MongoAbility,
|
MongoAbility,
|
||||||
RawRuleOf,
|
RawRuleOf,
|
||||||
|
buildMongoQueryMatcher,
|
||||||
createMongoAbility
|
createMongoAbility
|
||||||
} from "@casl/ability";
|
} from "@casl/ability";
|
||||||
import { Membership } from "../models";
|
import { Membership } from "../models";
|
||||||
import { IRole } from "../models/role";
|
import { IRole } from "../models/role";
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
|
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
|
||||||
|
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
||||||
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
|
const $glob: FieldInstruction<string> = {
|
||||||
|
type: "field",
|
||||||
|
validate(instruction, value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(`"${instruction.name}" expects value to be a string`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
||||||
|
const secretPath = context.get(object, node.field);
|
||||||
|
const permissionSecretGlobPath = node.value;
|
||||||
|
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||||
|
|
||||||
export enum ProjectPermissionActions {
|
export enum ProjectPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
@@ -36,7 +56,7 @@ export enum ProjectPermissionSub {
|
|||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
@@ -144,7 +164,7 @@ const buildAdminPermission = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace);
|
||||||
|
|
||||||
return build();
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const adminProjectPermissions = buildAdminPermission();
|
export const adminProjectPermissions = buildAdminPermission();
|
||||||
@@ -180,7 +200,7 @@ const buildMemberPermission = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
return build();
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const memberProjectPermissions = buildMemberPermission();
|
export const memberProjectPermissions = buildMemberPermission();
|
||||||
@@ -203,7 +223,7 @@ const buildViewerPermission = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
return build();
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const viewerProjectPermission = buildViewerPermission();
|
export const viewerProjectPermission = buildViewerPermission();
|
||||||
@@ -228,7 +248,9 @@ export const getUserProjectPermissions = async (userId: string, workspaceId: str
|
|||||||
if (membership.role === "viewer") return { permission: viewerProjectPermission, membership };
|
if (membership.role === "viewer") return { permission: viewerProjectPermission, membership };
|
||||||
|
|
||||||
if (membership.role === "custom") {
|
if (membership.role === "custom") {
|
||||||
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions);
|
const permission = createMongoAbility<ProjectPermissionSet>(membership.customRole.permissions, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
return { permission, membership };
|
return { permission, membership };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MembershipOrg } from "../models";
|
|||||||
import { IRole } from "../models/role";
|
import { IRole } from "../models/role";
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
|
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
|
||||||
import { ACCEPTED } from "../variables";
|
import { ACCEPTED } from "../variables";
|
||||||
|
import { conditionsMatcher } from "./ProjectRoleService";
|
||||||
|
|
||||||
export enum OrgPermissionActions {
|
export enum OrgPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
@@ -74,7 +75,7 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
return build();
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const adminPermissions = buildAdminPermission();
|
export const adminPermissions = buildAdminPermission();
|
||||||
@@ -92,7 +93,7 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||||
|
|
||||||
return build();
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const memberPermissions = buildMemberPermission();
|
export const memberPermissions = buildMemberPermission();
|
||||||
@@ -119,7 +120,9 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
|
|||||||
if (membership.role === "member") return { permission: memberPermissions, membership };
|
if (membership.role === "member") return { permission: memberPermissions, membership };
|
||||||
|
|
||||||
if (membership.role === "custom") {
|
if (membership.role === "custom") {
|
||||||
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions);
|
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
return { permission, membership };
|
return { permission, membership };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export const CreateRoleSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
subject: z.string().trim(),
|
subject: z.string().trim(),
|
||||||
action: z.string().trim(),
|
action: z.string().trim(),
|
||||||
conditions: z.record(z.union([z.string().trim(), z.number()])).optional()
|
conditions: z
|
||||||
|
.record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })]))
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
@@ -31,7 +33,9 @@ export const UpdateRoleSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
subject: z.string().trim(),
|
subject: z.string().trim(),
|
||||||
action: z.string().trim(),
|
action: z.string().trim(),
|
||||||
conditions: z.record(z.union([z.string().trim(), z.number()])).optional()
|
conditions: z
|
||||||
|
.record(z.union([z.string().trim(), z.number(), z.object({ $glob: z.string() })]))
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export const BatchSecretsV2 = z.object({
|
|||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
folderId: z.string().trim().default("root"),
|
folderId: z.string().trim().default("root"),
|
||||||
environment: z.string().trim(),
|
environment: z.string().trim(),
|
||||||
secretPath: z.string().trim().optional(),
|
secretPath: z.string().trim().default("/"),
|
||||||
requests: z
|
requests: z
|
||||||
.discriminatedUnion("method", [
|
.discriminatedUnion("method", [
|
||||||
z.object({
|
z.object({
|
||||||
@@ -328,7 +328,7 @@ export const CreateSecretV3 = z.object({
|
|||||||
secretCommentCiphertext: z.string().trim().optional(),
|
secretCommentCiphertext: z.string().trim().optional(),
|
||||||
secretCommentIV: z.string().trim().optional(),
|
secretCommentIV: z.string().trim().optional(),
|
||||||
secretCommentTag: z.string().trim().optional(),
|
secretCommentTag: z.string().trim().optional(),
|
||||||
metadata: z.record(z.string()).optional(),
|
metadata: z.record(z.string()).optional()
|
||||||
}),
|
}),
|
||||||
params: z.object({
|
params: z.object({
|
||||||
secretName: z.string().trim()
|
secretName: z.string().trim()
|
||||||
|
|||||||
Generated
+15
@@ -41,6 +41,7 @@
|
|||||||
"@stripe/stripe-js": "^1.46.0",
|
"@stripe/stripe-js": "^1.46.0",
|
||||||
"@tanstack/react-query": "^4.23.0",
|
"@tanstack/react-query": "^4.23.0",
|
||||||
"@types/argon2-browser": "^1.18.1",
|
"@types/argon2-browser": "^1.18.1",
|
||||||
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"argon2-browser": "^1.18.0",
|
"argon2-browser": "^1.18.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"next": "^12.3.4",
|
"next": "^12.3.4",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
"picomatch": "^2.3.1",
|
||||||
"posthog-js": "^1.58.0",
|
"posthog-js": "^1.58.0",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
@@ -106,6 +108,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.4",
|
"@tailwindcss/typography": "^0.5.4",
|
||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
|
"@types/picomatch": "^2.3.0",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
@@ -8391,6 +8394,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
|
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/picomatch": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/pretty-hrtime": {
|
"node_modules/@types/pretty-hrtime": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz",
|
||||||
@@ -29414,6 +29423,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||||
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
|
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
|
||||||
},
|
},
|
||||||
|
"@types/picomatch": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/pretty-hrtime": {
|
"@types/pretty-hrtime": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"@stripe/stripe-js": "^1.46.0",
|
"@stripe/stripe-js": "^1.46.0",
|
||||||
"@tanstack/react-query": "^4.23.0",
|
"@tanstack/react-query": "^4.23.0",
|
||||||
"@types/argon2-browser": "^1.18.1",
|
"@types/argon2-browser": "^1.18.1",
|
||||||
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"argon2-browser": "^1.18.0",
|
"argon2-browser": "^1.18.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"next": "^12.3.4",
|
"next": "^12.3.4",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
"picomatch": "^2.3.1",
|
||||||
"posthog-js": "^1.58.0",
|
"posthog-js": "^1.58.0",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
@@ -114,6 +116,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.4",
|
"@tailwindcss/typography": "^0.5.4",
|
||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
|
"@types/picomatch": "^2.3.0",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
|
|||||||
@@ -1,78 +1,92 @@
|
|||||||
|
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { Checkbox, PopoverContent } from "@app/components/v2";
|
import { Button, Checkbox, PopoverContent } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
import { WsTag } from "../../hooks/api/tags/types";
|
import { WsTag } from "../../hooks/api/tags/types";
|
||||||
|
import { ProjectPermissionCan } from "../permissions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
wsTags: WsTag[] | undefined;
|
wsTags: WsTag[] | undefined;
|
||||||
secKey: string;
|
secKey: string;
|
||||||
selectedTagIds: Record<string, boolean>;
|
selectedTagIds: Record<string, boolean>;
|
||||||
handleSelectTag: (wsTag: WsTag) => void;
|
handleSelectTag: (wsTag: WsTag) => void;
|
||||||
handleTagOnMouseEnter: (wsTag: WsTag) => void;
|
handleTagOnMouseEnter: (wsTag: WsTag) => void;
|
||||||
handleTagOnMouseLeave: () => void;
|
handleTagOnMouseLeave: () => void;
|
||||||
checkIfTagIsVisible: (wsTag: WsTag) => boolean;
|
checkIfTagIsVisible: (wsTag: WsTag) => boolean;
|
||||||
handleOnCreateTagOpen: () => void
|
handleOnCreateTagOpen: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddTagPopoverContent = ({
|
const AddTagPopoverContent = ({
|
||||||
wsTags,
|
wsTags,
|
||||||
secKey,
|
secKey,
|
||||||
selectedTagIds,
|
selectedTagIds,
|
||||||
handleSelectTag,
|
handleSelectTag,
|
||||||
handleTagOnMouseEnter,
|
handleTagOnMouseEnter,
|
||||||
handleTagOnMouseLeave,
|
handleTagOnMouseLeave,
|
||||||
checkIfTagIsVisible,
|
checkIfTagIsVisible,
|
||||||
handleOnCreateTagOpen
|
handleOnCreateTagOpen
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
side="left"
|
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"
|
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
|
hideCloseBtn
|
||||||
>
|
>
|
||||||
<div className=" text-center text-sm font-medium text-bunker-200">
|
<div className=" text-center text-sm font-medium text-bunker-200">
|
||||||
Add tags to {secKey || "this secret"}
|
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>
|
||||||
<div className="absolute left-0 w-full border-mineshaft-600 border-t mt-2" />
|
</div>
|
||||||
<div className="flex flex-col space-y-1.5">
|
))}
|
||||||
{wsTags?.map((wsTag: WsTag) => (
|
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
|
||||||
<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"
|
{(isAllowed) => (
|
||||||
onClick={() => handleSelectTag(wsTag)}
|
<Button
|
||||||
onMouseEnter={() => handleTagOnMouseEnter(wsTag)}
|
onClick={() => handleOnCreateTagOpen()}
|
||||||
onMouseLeave={() => handleTagOnMouseLeave()}
|
isDisabled={!isAllowed}
|
||||||
tabIndex={0} role="button"
|
size="xs"
|
||||||
onKeyDown={() => { }}>
|
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
|
export default AddTagPopoverContent;
|
||||||
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
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
containerClassName?: string;
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PermissionDeniedBanner = ({ containerClassName, className, children }: Props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"container h-full mx-auto flex justify-center items-center",
|
||||||
|
containerClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-4xl font-medium mb-2">Permission Denied</div>
|
||||||
|
{children || (
|
||||||
|
<div className="text-sm">
|
||||||
|
You do not have permission. <br /> Kindly contact your organization administrator
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -11,7 +11,11 @@ type Props = {
|
|||||||
// so when permission is allowed same tooltip will be reused to show helpertext
|
// so when permission is allowed same tooltip will be reused to show helpertext
|
||||||
renderTooltip?: boolean;
|
renderTooltip?: boolean;
|
||||||
allowedLabel?: string;
|
allowedLabel?: string;
|
||||||
} & BoundCanProps<TProjectPermission>;
|
// BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission
|
||||||
|
// For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error
|
||||||
|
// tried a lot combinations
|
||||||
|
// REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15
|
||||||
|
} & BoundCanProps<any>;
|
||||||
|
|
||||||
export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
||||||
label = "Permission Denied. Kindly contact your project admin",
|
label = "Permission Denied. Kindly contact your project admin",
|
||||||
@@ -22,7 +26,6 @@ export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
|||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const permission = useProjectPermission();
|
const permission = useProjectPermission();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Can {...props} passThrough={passThrough} ability={props?.ability || permission}>
|
<Can {...props} passThrough={passThrough} ability={props?.ability || permission}>
|
||||||
{(isAllowed, ability) => {
|
{(isAllowed, ability) => {
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export { OrgPermissionCan } from "./OrgPermissionCan";
|
export { OrgPermissionCan } from "./OrgPermissionCan";
|
||||||
|
export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
|
||||||
export { ProjectPermissionCan } from "./ProjectPermissionCan";
|
export { ProjectPermissionCan } from "./ProjectPermissionCan";
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export enum ProjectPermissionSub {
|
|||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { buildMongoQueryMatcher, createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
||||||
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
|
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
|
||||||
@@ -13,6 +15,29 @@ import {
|
|||||||
TRole
|
TRole
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
const $glob: FieldInstruction<string> = {
|
||||||
|
type: "field",
|
||||||
|
validate(instruction, value) {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(`"${instruction.name}" expects value to be a string`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
||||||
|
const secretPath = context.get(object, node.field);
|
||||||
|
const permissionSecretGlobPath = node.value;
|
||||||
|
if (!secretPath) return false;
|
||||||
|
// console.log(
|
||||||
|
// secretPath,
|
||||||
|
// picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false }),
|
||||||
|
// permissionSecretGlobPath
|
||||||
|
// );
|
||||||
|
return picomatch.isMatch(secretPath, permissionSecretGlobPath, { strictSlashes: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||||
|
|
||||||
export const roleQueryKeys = {
|
export const roleQueryKeys = {
|
||||||
getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const,
|
getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const,
|
||||||
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
|
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
|
||||||
@@ -57,7 +82,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
|
|||||||
enabled: Boolean(orgId),
|
enabled: Boolean(orgId),
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const rule = unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(data);
|
const rule = unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(data);
|
||||||
const ability = createMongoAbility<OrgPermissionSet>(rule);
|
const ability = createMongoAbility<OrgPermissionSet>(rule, { conditionsMatcher });
|
||||||
return ability;
|
return ability;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -739,16 +739,26 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="addMembers"
|
name="addMembers"
|
||||||
defaultValue
|
defaultValue={false}
|
||||||
render={({ field: { onBlur, value, onChange } }) => (
|
render={({ field: { onBlur, value, onChange } }) => (
|
||||||
<Checkbox
|
<OrgPermissionCan
|
||||||
id="add-project-layout"
|
I={OrgPermissionActions.Read}
|
||||||
isChecked={value}
|
a={OrgPermissionSubjects.Member}
|
||||||
onCheckedChange={onChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
>
|
>
|
||||||
Add all members of my organization to this project
|
{(isAllowed) => (
|
||||||
</Checkbox>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -838,16 +838,26 @@ const OrganizationPage = withPermission(
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="addMembers"
|
name="addMembers"
|
||||||
defaultValue
|
defaultValue={false}
|
||||||
render={({ field: { onBlur, value, onChange } }) => (
|
render={({ field: { onBlur, value, onChange } }) => (
|
||||||
<Checkbox
|
<OrgPermissionCan
|
||||||
id="add-project-layout"
|
I={OrgPermissionActions.Read}
|
||||||
isChecked={value}
|
a={OrgPermissionSubjects.Member}
|
||||||
onCheckedChange={onChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
>
|
>
|
||||||
Add all members of my organization to this project
|
{(isAllowed) => (
|
||||||
</Checkbox>
|
<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>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
@@ -9,6 +10,8 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
|||||||
type Props = {
|
type Props = {
|
||||||
folders?: Array<{ id: string; name: string }>;
|
folders?: Array<{ id: string; name: string }>;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
onFolderUpdate: (folderId: string, name: string) => void;
|
onFolderUpdate: (folderId: string, name: string) => void;
|
||||||
onFolderDelete: (folderId: string, name: string) => void;
|
onFolderDelete: (folderId: string, name: string) => void;
|
||||||
onFolderOpen: (folderId: string) => void;
|
onFolderOpen: (folderId: string) => void;
|
||||||
@@ -20,7 +23,9 @@ export const FolderSection = memo(
|
|||||||
onFolderDelete: handleFolderDelete,
|
onFolderDelete: handleFolderDelete,
|
||||||
onFolderOpen: handleFolderOpen,
|
onFolderOpen: handleFolderOpen,
|
||||||
search = "",
|
search = "",
|
||||||
folders = []
|
folders = [],
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -51,7 +56,7 @@ export const FolderSection = memo(
|
|||||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Folders}
|
a={subject(ProjectPermissionSub.Folders, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
@@ -72,7 +77,7 @@ export const FolderSection = memo(
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={ProjectPermissionSub.Folders}
|
a={subject(ProjectPermissionSub.Folders, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
|||||||
+9
-4
@@ -1,4 +1,5 @@
|
|||||||
import { useFormContext, useWatch } from "react-hook-form";
|
import { useFormContext, useWatch } from "react-hook-form";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons";
|
import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
@@ -23,6 +24,8 @@ import { GenRandomNumber } from "./GenRandomNumber";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isDrawerOpen: boolean;
|
isDrawerOpen: boolean;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
index: number;
|
index: number;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
@@ -41,7 +44,9 @@ export const SecretDetailDrawer = ({
|
|||||||
isReadOnly,
|
isReadOnly,
|
||||||
onSecretDelete,
|
onSecretDelete,
|
||||||
onSave,
|
onSave,
|
||||||
onEnvCompare
|
onEnvCompare,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const [canRevealSecVal, setCanRevealSecVal] = useToggle();
|
const [canRevealSecVal, setCanRevealSecVal] = useToggle();
|
||||||
const [canRevealSecOverride, setCanRevealSecOverride] = useToggle();
|
const [canRevealSecOverride, setCanRevealSecOverride] = useToggle();
|
||||||
@@ -89,7 +94,7 @@ export const SecretDetailDrawer = ({
|
|||||||
<div className="flex w-full space-x-2">
|
<div className="flex w-full space-x-2">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Secrets}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly || !isAllowed}>
|
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly || !isAllowed}>
|
||||||
@@ -98,8 +103,8 @@ export const SecretDetailDrawer = ({
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={ProjectPermissionSub.Secrets}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ChangeEvent, DragEvent, useEffect, useState } from "react";
|
import { ChangeEvent, DragEvent, useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
||||||
import {
|
import {
|
||||||
faClone,
|
faClone,
|
||||||
@@ -78,6 +79,8 @@ type Props = {
|
|||||||
environments?: { name: string; slug: string }[];
|
environments?: { name: string; slug: string }[];
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
decryptFileKey: UserWsKeyPair;
|
decryptFileKey: UserWsKeyPair;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretDropzone = ({
|
export const SecretDropzone = ({
|
||||||
@@ -86,7 +89,9 @@ export const SecretDropzone = ({
|
|||||||
onAddNewSecret,
|
onAddNewSecret,
|
||||||
environments = [],
|
environments = [],
|
||||||
workspaceId,
|
workspaceId,
|
||||||
decryptFileKey
|
decryptFileKey,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isDragActive, setDragActive] = useToggle();
|
const [isDragActive, setDragActive] = useToggle();
|
||||||
@@ -109,16 +114,16 @@ export const SecretDropzone = ({
|
|||||||
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
|
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretPath = watch("secretPath");
|
const envCopySecPath = watch("secretPath");
|
||||||
const selectedEnvSlug = watch("environment");
|
const selectedEnvSlug = watch("environment");
|
||||||
const debouncedSecretPath = useDebounce(secretPath);
|
const debouncedEnvCopySecretPath = useDebounce(envCopySecPath);
|
||||||
|
|
||||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
env: selectedEnvSlug,
|
env: selectedEnvSlug,
|
||||||
secretPath: debouncedSecretPath,
|
secretPath: debouncedEnvCopySecretPath,
|
||||||
isPaused:
|
isPaused:
|
||||||
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) &&
|
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedEnvCopySecretPath)) &&
|
||||||
!popUp.importSecEnv.isOpen,
|
!popUp.importSecEnv.isOpen,
|
||||||
decryptFileKey
|
decryptFileKey
|
||||||
});
|
});
|
||||||
@@ -126,7 +131,7 @@ export const SecretDropzone = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue("secrets", {});
|
setValue("secrets", {});
|
||||||
setSearchFilter("");
|
setSearchFilter("");
|
||||||
}, [debouncedSecretPath]);
|
}, [debouncedEnvCopySecretPath]);
|
||||||
|
|
||||||
const handleDrag = (e: DragEvent) => {
|
const handleDrag = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -238,7 +243,7 @@ export const SecretDropzone = ({
|
|||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.Secrets}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<input
|
<input
|
||||||
@@ -271,16 +276,22 @@ export const SecretDropzone = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalTrigger asChild>
|
<ModalTrigger asChild>
|
||||||
<ProjectPermissionCan
|
<div>
|
||||||
I={ProjectPermissionActions.Create}
|
<ProjectPermissionCan
|
||||||
a={ProjectPermissionSub.Secrets}
|
I={ProjectPermissionActions.Create}
|
||||||
>
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
{(isAllowed) => (
|
>
|
||||||
<Button isDisabled={!isAllowed} variant="star" size={isSmaller ? "xs" : "sm"}>
|
{(isAllowed) => (
|
||||||
Copy Secrets From An Environment
|
<Button
|
||||||
</Button>
|
isDisabled={!isAllowed}
|
||||||
)}
|
variant="star"
|
||||||
</ProjectPermissionCan>
|
size={isSmaller ? "xs" : "sm"}
|
||||||
|
>
|
||||||
|
Copy Secrets From An Environment
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
</ModalTrigger>
|
</ModalTrigger>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
className="max-w-2xl"
|
className="max-w-2xl"
|
||||||
@@ -416,7 +427,7 @@ export const SecretDropzone = ({
|
|||||||
{!isSmaller && (
|
{!isSmaller && (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.Secrets}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button variant="star" onClick={onAddNewSecret} isDisabled={!isAllowed}>
|
<Button variant="star" onClick={onAddNewSecret} isDisabled={!isAllowed}>
|
||||||
|
|||||||
+8
-3
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import {
|
import {
|
||||||
faFileImport,
|
faFileImport,
|
||||||
@@ -11,11 +12,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub,useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDelete: (environment: string, secretPath: string) => void;
|
onDelete: (environment: string, secretPath: string) => void;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
importedEnv: string;
|
importedEnv: string;
|
||||||
importedSecPath: string;
|
importedSecPath: string;
|
||||||
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
||||||
@@ -40,7 +43,9 @@ export const SecretImportItem = ({
|
|||||||
importedSecPath,
|
importedSecPath,
|
||||||
onDelete,
|
onDelete,
|
||||||
importedSecrets = [],
|
importedSecrets = [],
|
||||||
searchTerm = ""
|
searchTerm = "",
|
||||||
|
secretPath,
|
||||||
|
environment
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isExpanded, setIsExpanded] = useToggle();
|
const [isExpanded, setIsExpanded] = useToggle();
|
||||||
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
||||||
@@ -114,7 +119,7 @@ export const SecretImportItem = ({
|
|||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={ProjectPermissionSub.SecretImports}
|
a={subject(ProjectPermissionSub.SecretImports, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
|||||||
+13
-1
@@ -59,13 +59,23 @@ export const computeImportedSecretRows = (
|
|||||||
type Props = {
|
type Props = {
|
||||||
secrets?: DecryptedSecret[];
|
secrets?: DecryptedSecret[];
|
||||||
importedSecrets?: TImportedSecrets;
|
importedSecrets?: TImportedSecrets;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
onSecretImportDelete: (env: string, secPath: string) => void;
|
onSecretImportDelete: (env: string, secPath: string) => void;
|
||||||
items: { id: string; environment: string; secretPath: string }[];
|
items: { id: string; environment: string; secretPath: string }[];
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretImportSection = memo(
|
export const SecretImportSection = memo(
|
||||||
({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [], searchTerm = "" }: Props) => {
|
({
|
||||||
|
secrets = [],
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
importedSecrets = [],
|
||||||
|
onSecretImportDelete,
|
||||||
|
items = [],
|
||||||
|
searchTerm = ""
|
||||||
|
}: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const environments = currentWorkspace?.environments || [];
|
const environments = currentWorkspace?.environments || [];
|
||||||
|
|
||||||
@@ -82,6 +92,8 @@ export const SecretImportSection = memo(
|
|||||||
secrets,
|
secrets,
|
||||||
environments
|
environments
|
||||||
)}
|
)}
|
||||||
|
secretPath={secretPath}
|
||||||
|
environment={environment}
|
||||||
onDelete={onSecretImportDelete}
|
onDelete={onSecretImportDelete}
|
||||||
importedSecPath={impSecPath}
|
importedSecPath={impSecPath}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
UseFormSetValue,
|
UseFormSetValue,
|
||||||
useWatch
|
useWatch
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
@@ -22,31 +23,34 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { cx } from "cva";
|
import { cx } from "cva";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
// TODO:(akhilmhdh): Refactor this
|
||||||
|
import AddTagPopoverContent from "@app/components/AddTagPopoverContent/AddTagPopoverContent";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
FormControl,
|
||||||
HoverCard,
|
HoverCard,
|
||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Popover,
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
SecretInput,
|
SecretInput,
|
||||||
Tag,
|
Tag,
|
||||||
|
TextArea,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { WsTag } from "@app/hooks/api/types";
|
import { WsTag } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent";
|
|
||||||
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
index: number;
|
index: number;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
// backend generated unique id
|
// backend generated unique id
|
||||||
secUniqId?: string;
|
secUniqId?: string;
|
||||||
// permission and external state's that decided to hide or show
|
// permission and external state's that decided to hide or show
|
||||||
@@ -74,6 +78,8 @@ type Props = {
|
|||||||
export const SecretInputRow = memo(
|
export const SecretInputRow = memo(
|
||||||
({
|
({
|
||||||
index,
|
index,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
isSecretValueHidden,
|
isSecretValueHidden,
|
||||||
onRowExpand,
|
onRowExpand,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
@@ -84,7 +90,7 @@ export const SecretInputRow = memo(
|
|||||||
onSecretDelete,
|
onSecretDelete,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
control,
|
control,
|
||||||
// register,
|
register,
|
||||||
setValue,
|
setValue,
|
||||||
isKeyError,
|
isKeyError,
|
||||||
keyError,
|
keyError,
|
||||||
@@ -222,7 +228,6 @@ export const SecretInputRow = memo(
|
|||||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||||
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
@@ -361,16 +366,24 @@ export const SecretInputRow = memo(
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<div className="w-0 group-hover:w-6 data-[state=open]:w-6">
|
<div className="w-0 group-hover:w-6 data-[state=open]:w-6">
|
||||||
<Tooltip content="Add tags">
|
<ProjectPermissionCan
|
||||||
<IconButton
|
renderTooltip
|
||||||
variant="plain"
|
allowedLabel="Add Tags"
|
||||||
size="md"
|
I={ProjectPermissionActions.Edit}
|
||||||
ariaLabel="add-tag"
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
className="py-[0.42rem]"
|
>
|
||||||
>
|
{(isAllowed) => (
|
||||||
<FontAwesomeIcon icon={faTags} />
|
<IconButton
|
||||||
</IconButton>
|
isDisabled={!isAllowed}
|
||||||
</Tooltip>
|
variant="plain"
|
||||||
|
size="md"
|
||||||
|
ariaLabel="add-tags"
|
||||||
|
className="py-[0.42rem]"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTags} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<AddTagPopoverContent
|
<AddTagPopoverContent
|
||||||
@@ -390,55 +403,76 @@ export const SecretInputRow = memo(
|
|||||||
<div className="flex h-8 flex-row items-center pr-2">
|
<div className="flex h-8 flex-row items-center pr-2">
|
||||||
{!isAddOnly && (
|
{!isAddOnly && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content="Override with a personal value">
|
<ProjectPermissionCan
|
||||||
<IconButton
|
renderTooltip
|
||||||
variant="plain"
|
allowedLabel="Override with a personal value"
|
||||||
className={twMerge(
|
I={ProjectPermissionActions.Edit}
|
||||||
"mt-0.5 w-0 overflow-hidden p-0 group-hover:ml-1 group-hover:w-7",
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
isOverridden && "ml-1 w-7 text-primary"
|
>
|
||||||
)}
|
{(isAllowed) => (
|
||||||
onClick={onSecretOverride}
|
<IconButton
|
||||||
size="md"
|
variant="plain"
|
||||||
isDisabled={isRollbackMode || isReadOnly}
|
className={twMerge(
|
||||||
ariaLabel="info"
|
"mt-0.5 w-0 overflow-hidden p-0 group-hover:ml-1 group-hover:w-7",
|
||||||
>
|
isOverridden && "ml-1 w-7 text-primary"
|
||||||
<div className="flex items-center space-x-1">
|
)}
|
||||||
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
|
onClick={onSecretOverride}
|
||||||
</div>
|
size="md"
|
||||||
</IconButton>
|
isDisabled={isRollbackMode || isReadOnly || !isAllowed}
|
||||||
</Tooltip>
|
ariaLabel="info"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
|
||||||
|
</div>
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Tooltip content="Comment">
|
<div className="mt-0.5 overflow-hidden ">
|
||||||
<div className="mt-0.5 overflow-hidden ">
|
<Popover>
|
||||||
<Popover>
|
<PopoverTrigger asChild>
|
||||||
<PopoverTrigger asChild>
|
<div>
|
||||||
<IconButton
|
<ProjectPermissionCan
|
||||||
className={twMerge(
|
renderTooltip
|
||||||
"w-7 overflow-hidden p-0",
|
allowedLabel="Comment"
|
||||||
"w-0 group-hover:w-7 data-[state=open]:w-7",
|
I={ProjectPermissionActions.Edit}
|
||||||
hasComment ? "w-7 text-primary" : "group-hover:w-7"
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
)}
|
|
||||||
variant="plain"
|
|
||||||
size="md"
|
|
||||||
ariaLabel="add-tag"
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faComment} />
|
{(isAllowed) => (
|
||||||
</IconButton>
|
<IconButton
|
||||||
</PopoverTrigger>
|
className={twMerge(
|
||||||
<AddTagPopoverContent
|
"w-7 overflow-hidden p-0",
|
||||||
wsTags={wsTags}
|
"w-0 group-hover:w-7 data-[state=open]:w-7",
|
||||||
secKey={secKey || "this secret"}
|
hasComment ? "w-7 text-primary" : "group-hover:w-7"
|
||||||
selectedTagIds={selectedTagIds}
|
)}
|
||||||
handleSelectTag={(wsTag: WsTag) => onSelectTag(wsTag)}
|
isDisabled={!isAllowed}
|
||||||
handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)}
|
variant="plain"
|
||||||
handleTagOnMouseLeave={() => handleTagOnMouseLeave()}
|
size="md"
|
||||||
checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)}
|
ariaLabel="add-comment"
|
||||||
handleOnCreateTagOpen={() => onCreateTagOpen()}
|
>
|
||||||
/>
|
<FontAwesomeIcon icon={faComment} />
|
||||||
</Popover>
|
</IconButton>
|
||||||
</div>
|
)}
|
||||||
</Tooltip>
|
</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>
|
||||||
<div className="duration-0 flex w-16 justify-center overflow-hidden border-l border-mineshaft-600 pl-2 transition-all">
|
<div className="duration-0 flex w-16 justify-center overflow-hidden border-l border-mineshaft-600 pl-2 transition-all">
|
||||||
<div className="flex h-8 items-center space-x-2.5">
|
<div className="flex h-8 items-center space-x-2.5">
|
||||||
@@ -457,29 +491,29 @@ export const SecretInputRow = memo(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ProjectPermissionCan
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
I={ProjectPermissionActions.Delete}
|
<ProjectPermissionCan
|
||||||
a={ProjectPermissionSub.Secrets}
|
I={ProjectPermissionActions.Delete}
|
||||||
>
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
{(isAllowed) => (
|
renderTooltip
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
allowedLabel="Delete"
|
||||||
<Tooltip content="Delete">
|
>
|
||||||
<IconButton
|
{(isAllowed) => (
|
||||||
size="lg"
|
<IconButton
|
||||||
variant="plain"
|
size="lg"
|
||||||
colorSchema="danger"
|
variant="plain"
|
||||||
ariaLabel="delete"
|
colorSchema="danger"
|
||||||
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
|
ariaLabel="delete"
|
||||||
onClick={() => {
|
isDisabled={isReadOnly || isRollbackMode || !isAllowed}
|
||||||
onSecretDelete(index, secKey, secId, idOverride);
|
onClick={() => {
|
||||||
}}
|
onSecretDelete(index, secKey, secId, idOverride);
|
||||||
>
|
}}
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
>
|
||||||
</IconButton>
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
</div>
|
)}
|
||||||
)}
|
</ProjectPermissionCan>
|
||||||
</ProjectPermissionCan>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
+10
-2
@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
@@ -68,7 +69,7 @@ export const MultiEnvProjectPermission = ({
|
|||||||
const handlePermissionChange = (val: Permission) => {
|
const handlePermissionChange = (val: Permission) => {
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case Permission.NoAccess:
|
case Permission.NoAccess:
|
||||||
setValue(`permissions.${formName}`, {}, { shouldDirty: true });
|
setValue(`permissions.${formName}`, undefined, { shouldDirty: true });
|
||||||
break;
|
break;
|
||||||
case Permission.FullAccess:
|
case Permission.FullAccess:
|
||||||
setValue(
|
setValue(
|
||||||
@@ -151,7 +152,14 @@ export const MultiEnvProjectPermission = ({
|
|||||||
name={`permissions.${formName}.${slug}.secretPath`}
|
name={`permissions.${formName}.${slug}.secretPath`}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input {...field} className="w-full overflow-ellipsis" />
|
/* eslint-disable-next-line no-template-curly-in-string */
|
||||||
|
<FormControl helperText="Ex pattern: /, /**, /{folder1,folder2}">
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
className="w-full overflow-ellipsis"
|
||||||
|
placeholder="Glob patterns are supported"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
|
|||||||
+1
@@ -133,6 +133,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
const handleRoleUpdate = async (el: TFormSchema) => {
|
const handleRoleUpdate = async (el: TFormSchema) => {
|
||||||
if (!role?._id) return;
|
if (!role?._id) return;
|
||||||
|
|
||||||
|
console.log(el);
|
||||||
try {
|
try {
|
||||||
await updateRole({
|
await updateRole({
|
||||||
orgId,
|
orgId,
|
||||||
|
|||||||
+18
-10
@@ -73,7 +73,7 @@ const multiEnvApi2Form = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const secretEnv = permission?.conditions?.environment || "all";
|
const secretEnv = permission?.conditions?.environment || "all";
|
||||||
const secretPath = permission?.conditions?.secretPath;
|
const secretPath = permission?.conditions?.secretPath?.$glob;
|
||||||
// initialize
|
// initialize
|
||||||
if (formVal && !formVal?.[secretEnv]) {
|
if (formVal && !formVal?.[secretEnv]) {
|
||||||
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
|
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
|
||||||
@@ -109,6 +109,8 @@ const multiEnvForm2Api = (
|
|||||||
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
||||||
subject: (typeof MULTI_ENV_KEY)[number]
|
subject: (typeof MULTI_ENV_KEY)[number]
|
||||||
) => {
|
) => {
|
||||||
|
if (!formVal) return;
|
||||||
|
|
||||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
||||||
// if any of them is set in all push it without any condition
|
// if any of them is set in all push it without any condition
|
||||||
PERMISSION_ACTIONS.forEach((action) => {
|
PERMISSION_ACTIONS.forEach((action) => {
|
||||||
@@ -130,7 +132,8 @@ const multiEnvForm2Api = (
|
|||||||
// if not full access for an action
|
// if not full access for an action
|
||||||
if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) {
|
if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) {
|
||||||
const conditions: Record<string, unknown> = { environment: slug };
|
const conditions: Record<string, unknown> = { environment: slug };
|
||||||
if (formVal[slug]?.secretPath) conditions.secretPath = formVal?.[slug]?.secretPath;
|
if (formVal[slug]?.secretPath)
|
||||||
|
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
|
||||||
|
|
||||||
permissions.push({ action, subject, conditions });
|
permissions.push({ action, subject, conditions });
|
||||||
}
|
}
|
||||||
@@ -141,17 +144,22 @@ const multiEnvForm2Api = (
|
|||||||
|
|
||||||
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||||
const permissions: TProjectPermission[] = [];
|
const permissions: TProjectPermission[] = [];
|
||||||
MULTI_ENV_KEY.forEach((formName) => {
|
|
||||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(formVal?.[formName] || {})), formName);
|
|
||||||
});
|
|
||||||
// other than workspace everything else follows same
|
// other than workspace everything else follows same
|
||||||
// if in future there is a different follow the above on how workspace is done
|
// if in future there is a different follow the above on how workspace is done
|
||||||
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
||||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
if (MULTI_ENV_KEY.includes(rule as (typeof MULTI_ENV_KEY)[number])) {
|
||||||
if (isAllowed) {
|
multiEnvForm2Api(
|
||||||
permissions.push({ subject: rule, action });
|
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;
|
return permissions;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
|
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
@@ -36,7 +37,6 @@ import {
|
|||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
|
||||||
import {
|
import {
|
||||||
useCreateSecretV3,
|
useCreateSecretV3,
|
||||||
useDeleteSecretV3,
|
useDeleteSecretV3,
|
||||||
@@ -50,7 +50,7 @@ import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
|||||||
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||||
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
||||||
|
|
||||||
const SecretOverview = () => {
|
export const SecretOverviewPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -91,11 +91,20 @@ const SecretOverview = () => {
|
|||||||
}, [isWorkspaceLoading, workspaceId, router.isReady]);
|
}, [isWorkspaceLoading, workspaceId, router.isReady]);
|
||||||
|
|
||||||
const userAvailableEnvs =
|
const userAvailableEnvs =
|
||||||
currentWorkspace?.environments?.filter(({ slug }) =>
|
currentWorkspace?.environments?.filter(
|
||||||
permission.can(
|
({ slug }) =>
|
||||||
ProjectPermissionActions.Read,
|
permission.can(
|
||||||
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath: secretPath || "/" })
|
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 {
|
const {
|
||||||
@@ -371,18 +380,23 @@ const SecretOverview = () => {
|
|||||||
onClick={handleFolderClick}
|
onClick={handleFolderClick}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{filteredSecretNames.map((key, index) => (
|
{userAvailableEnvs?.length > 0 ? (
|
||||||
<SecretOverviewTableRow
|
filteredSecretNames.map((key, index) => (
|
||||||
onSecretCreate={handleSecretCreate}
|
<SecretOverviewTableRow
|
||||||
onSecretDelete={handleSecretDelete}
|
secretPath={secretPath}
|
||||||
onSecretUpdate={handleSecretUpdate}
|
onSecretCreate={handleSecretCreate}
|
||||||
key={`overview-${key}-${index + 1}`}
|
onSecretDelete={handleSecretDelete}
|
||||||
environments={userAvailableEnvs}
|
onSecretUpdate={handleSecretUpdate}
|
||||||
secretKey={key}
|
key={`overview-${key}-${index + 1}`}
|
||||||
getSecretByKey={getSecretByKey}
|
environments={userAvailableEnvs}
|
||||||
expandableColWidth={expandableTableWidth}
|
secretKey={key}
|
||||||
/>
|
getSecretByKey={getSecretByKey}
|
||||||
))}
|
expandableColWidth={expandableTableWidth}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<PermissionDeniedBanner />
|
||||||
|
)}
|
||||||
</TBody>
|
</TBody>
|
||||||
<TFoot>
|
<TFoot>
|
||||||
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
|
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
|
||||||
@@ -414,8 +428,3 @@ const SecretOverview = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretOverviewPage = withProjectPermission(SecretOverview, {
|
|
||||||
action: ProjectPermissionActions.Read,
|
|
||||||
subject: ProjectPermissionSub.Secrets
|
|
||||||
});
|
|
||||||
|
|||||||
+4
-1
@@ -1,4 +1,5 @@
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ type Props = {
|
|||||||
isCreatable?: boolean;
|
isCreatable?: boolean;
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
environment: string;
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
|
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
|
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
onSecretDelete: (env: string, key: string) => Promise<void>;
|
onSecretDelete: (env: string, key: string) => Promise<void>;
|
||||||
@@ -27,6 +29,7 @@ export const SecretEditRow = ({
|
|||||||
onSecretCreate,
|
onSecretCreate,
|
||||||
onSecretDelete,
|
onSecretDelete,
|
||||||
environment,
|
environment,
|
||||||
|
secretPath,
|
||||||
isVisible
|
isVisible
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
@@ -95,7 +98,7 @@ export const SecretEditRow = ({
|
|||||||
<>
|
<>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.Secrets}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+8
-3
@@ -18,6 +18,7 @@ import { SecretEditRow } from "./SecretEditRow";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
|
secretPath: string;
|
||||||
environments: { name: string; slug: string }[];
|
environments: { name: string; slug: string }[];
|
||||||
expandableColWidth: number;
|
expandableColWidth: number;
|
||||||
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
|
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
|
||||||
@@ -29,6 +30,7 @@ type Props = {
|
|||||||
export const SecretOverviewTableRow = ({
|
export const SecretOverviewTableRow = ({
|
||||||
secretKey,
|
secretKey,
|
||||||
environments = [],
|
environments = [],
|
||||||
|
secretPath,
|
||||||
getSecretByKey,
|
getSecretByKey,
|
||||||
onSecretUpdate,
|
onSecretUpdate,
|
||||||
onSecretCreate,
|
onSecretCreate,
|
||||||
@@ -73,9 +75,11 @@ export const SecretOverviewTableRow = ({
|
|||||||
>
|
>
|
||||||
<div className="h-full w-full border-r border-mineshaft-600 py-[0.85rem] px-5">
|
<div className="h-full w-full border-r border-mineshaft-600 py-[0.85rem] px-5">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
{!isSecretEmpty && <Tooltip content={isSecretPresent ? "Present secret" : "Missing secret"}>
|
{!isSecretEmpty && (
|
||||||
<FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />
|
<Tooltip content={isSecretPresent ? "Present secret" : "Missing secret"}>
|
||||||
</Tooltip>}
|
<FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{isSecretEmpty && (
|
{isSecretEmpty && (
|
||||||
<Tooltip content="Empty value">
|
<Tooltip content="Empty value">
|
||||||
<FontAwesomeIcon icon={faCircle} />
|
<FontAwesomeIcon icon={faCircle} />
|
||||||
@@ -141,6 +145,7 @@ export const SecretOverviewTableRow = ({
|
|||||||
</td>
|
</td>
|
||||||
<td className="col-span-2 h-8 w-full">
|
<td className="col-span-2 h-8 w-full">
|
||||||
<SecretEditRow
|
<SecretEditRow
|
||||||
|
secretPath={secretPath}
|
||||||
isVisible={isSecretVisible}
|
isVisible={isSecretVisible}
|
||||||
secretName={secretKey}
|
secretName={secretKey}
|
||||||
defaultValue={secret?.value}
|
defaultValue={secret?.value}
|
||||||
|
|||||||
+38
-42
@@ -2,53 +2,49 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan, PermissionDeniedBanner } from "@app/components/permissions";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
|
|
||||||
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
|
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
|
||||||
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
|
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
|
||||||
|
|
||||||
export const OrgIncidentContactsSection = withPermission(
|
export const OrgIncidentContactsSection = () => {
|
||||||
() => {
|
const { t } = useTranslation();
|
||||||
const { t } = useTranslation();
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
"addContact"
|
||||||
"addContact"
|
] as const);
|
||||||
] as const);
|
const permission = useOrgPermission();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex justify-between mb-4">
|
<div className="flex justify-between mb-4">
|
||||||
<p className="min-w-max text-xl font-semibold">
|
<p className="min-w-max text-xl font-semibold">{t("section.incident.incident-contacts")}</p>
|
||||||
{t("section.incident.incident-contacts")}
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}>
|
||||||
</p>
|
{(isAllowed) => (
|
||||||
<OrgPermissionCan
|
<Button
|
||||||
I={OrgPermissionActions.Create}
|
colorSchema="secondary"
|
||||||
a={OrgPermissionSubjects.IncidentAccount}
|
type="submit"
|
||||||
>
|
isDisabled={!isAllowed}
|
||||||
{(isAllowed) => (
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
<Button
|
onClick={() => handlePopUpOpen("addContact")}
|
||||||
colorSchema="secondary"
|
>
|
||||||
type="submit"
|
Add contact
|
||||||
isDisabled={!isAllowed}
|
</Button>
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
)}
|
||||||
onClick={() => handlePopUpOpen("addContact")}
|
</OrgPermissionCan>
|
||||||
>
|
|
||||||
Add contact
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</div>
|
|
||||||
<OrgIncidentContactsTable />
|
|
||||||
<AddOrgIncidentContactModal
|
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpClose={handlePopUpClose}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
{permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount) ? (
|
||||||
},
|
<OrgIncidentContactsTable />
|
||||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.IncidentAccount }
|
) : (
|
||||||
);
|
<PermissionDeniedBanner />
|
||||||
|
)}
|
||||||
|
<AddOrgIncidentContactModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+62
-70
@@ -7,7 +7,6 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
|
||||||
import { useRenameOrg } from "@app/hooks/api";
|
import { useRenameOrg } from "@app/hooks/api";
|
||||||
|
|
||||||
const formSchema = yup.object({
|
const formSchema = yup.object({
|
||||||
@@ -16,77 +15,70 @@ const formSchema = yup.object({
|
|||||||
|
|
||||||
type FormData = yup.InferType<typeof formSchema>;
|
type FormData = yup.InferType<typeof formSchema>;
|
||||||
|
|
||||||
export const OrgNameChangeSection = withPermission(
|
export const OrgNameChangeSection = (): JSX.Element => {
|
||||||
(): JSX.Element => {
|
const { currentOrg } = useOrganization();
|
||||||
const { currentOrg } = useOrganization();
|
const { createNotification } = useNotificationContext();
|
||||||
const { createNotification } = useNotificationContext();
|
const { handleSubmit, control, reset } = useForm<FormData>({
|
||||||
const { handleSubmit, control, reset } = useForm<FormData>({
|
resolver: yupResolver(formSchema)
|
||||||
resolver: yupResolver(formSchema)
|
});
|
||||||
});
|
const { mutateAsync, isLoading } = useRenameOrg();
|
||||||
const { mutateAsync, isLoading } = useRenameOrg();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentOrg) {
|
if (currentOrg) {
|
||||||
reset({ name: currentOrg.name });
|
reset({ name: currentOrg.name });
|
||||||
}
|
}
|
||||||
}, [currentOrg]);
|
}, [currentOrg]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ name }: FormData) => {
|
const onFormSubmit = async ({ name }: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
if (name === "") return;
|
if (name === "") return;
|
||||||
|
|
||||||
await mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
|
await mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully renamed organization",
|
text: "Successfully renamed organization",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to rename organization",
|
text: "Failed to rename organization",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
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>
|
<p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
|
||||||
<div className="mb-2 max-w-md">
|
<div className="mb-2 max-w-md">
|
||||||
<Controller
|
<Controller
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
<Input placeholder="Acme Corp" {...field} />
|
<Input placeholder="Acme Corp" {...field} />
|
||||||
</FormControl>
|
</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>
|
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
control={control}
|
||||||
</form>
|
name="name"
|
||||||
);
|
/>
|
||||||
},
|
</div>
|
||||||
{
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||||
action: OrgPermissionActions.Read,
|
{(isAllowed) => (
|
||||||
subject: OrgPermissionSubjects.Settings,
|
<Button
|
||||||
containerClassName: "mb-4"
|
isLoading={isLoading}
|
||||||
}
|
isDisabled={!isAllowed}
|
||||||
);
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
|
||||||
import { withProjectPermission } from "@app/hoc";
|
|
||||||
|
|
||||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||||
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
|
import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab";
|
||||||
@@ -16,50 +14,47 @@ const tabs = [
|
|||||||
{ name: "Webhooks", key: "tab-project-webhooks" }
|
{ name: "Webhooks", key: "tab-project-webhooks" }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ProjectSettingsPage = withProjectPermission(
|
export const ProjectSettingsPage = () => {
|
||||||
() => {
|
const { t } = useTranslation();
|
||||||
const { t } = useTranslation();
|
return (
|
||||||
return (
|
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
|
||||||
<div className="flex w-full justify-center bg-bunker-800 px-6 text-white">
|
<div className="w-full max-w-screen-lg">
|
||||||
<div className="w-full max-w-screen-lg">
|
<div className="relative right-5 ml-4">
|
||||||
<div className="relative right-5 ml-4">
|
<NavHeader pageName={t("settings.project.title")} isProjectRelated />
|
||||||
<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>
|
</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>
|
</div>
|
||||||
);
|
</div>
|
||||||
},
|
);
|
||||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
|
};
|
||||||
);
|
|
||||||
|
|||||||
+48
-52
@@ -4,61 +4,57 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { Checkbox } from "@app/components/v2";
|
import { Checkbox } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
|
||||||
import { useToggleAutoCapitalization } from "@app/hooks/api";
|
import { useToggleAutoCapitalization } from "@app/hooks/api";
|
||||||
|
|
||||||
export const AutoCapitalizationSection = withProjectPermission(
|
export const AutoCapitalizationSection = () => {
|
||||||
() => {
|
const { t } = useTranslation();
|
||||||
const { t } = useTranslation();
|
const { createNotification } = useNotificationContext();
|
||||||
const { createNotification } = useNotificationContext();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { mutateAsync } = useToggleAutoCapitalization();
|
||||||
const { mutateAsync } = useToggleAutoCapitalization();
|
|
||||||
|
|
||||||
const handleToggleCapitalizationToggle = async (state: boolean) => {
|
const handleToggleCapitalizationToggle = async (state: boolean) => {
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace?._id) return;
|
if (!currentWorkspace?._id) return;
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
workspaceID: currentWorkspace._id,
|
workspaceID: currentWorkspace._id,
|
||||||
state
|
state
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = `Successfully ${state ? "enabled" : "disabled"} auto capitalization`;
|
const text = `Successfully ${state ? "enabled" : "disabled"} auto capitalization`;
|
||||||
createNotification({
|
createNotification({
|
||||||
text,
|
text,
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to update auto capitalization",
|
text: "Failed to update auto capitalization",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||||
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
|
<p className="mb-3 text-xl font-semibold">{t("settings.project.auto-capitalization")}</p>
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div className="w-max">
|
<div className="w-max">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className="data-[state=checked]:bg-primary"
|
className="data-[state=checked]:bg-primary"
|
||||||
id="autoCapitalization"
|
id="autoCapitalization"
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
isChecked={currentWorkspace?.autoCapitalization ?? false}
|
isChecked={currentWorkspace?.autoCapitalization ?? false}
|
||||||
onCheckedChange={(state) => {
|
onCheckedChange={(state) => {
|
||||||
handleToggleCapitalizationToggle(state as boolean);
|
handleToggleCapitalizationToggle(state as boolean);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("settings.project.auto-capitalization-description")}
|
{t("settings.project.auto-capitalization-description")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
|
|
||||||
);
|
|
||||||
|
|||||||
+94
-101
@@ -5,113 +5,106 @@ import {
|
|||||||
} from "@app/components/utilities/cryptography/crypto";
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
import { Checkbox } from "@app/components/v2";
|
import { Checkbox } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
|
||||||
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
||||||
|
|
||||||
export const E2EESection = withProjectPermission(
|
export const E2EESection = () => {
|
||||||
() => {
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
|
||||||
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
|
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
|
||||||
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
|
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activate bot for project by performing the following steps:
|
* Activate bot for project by performing the following steps:
|
||||||
* 1. Get the (encrypted) project key
|
* 1. Get the (encrypted) project key
|
||||||
* 2. Decrypt project key with user's private key
|
* 2. Decrypt project key with user's private key
|
||||||
* 3. Encrypt project key with bot's public key
|
* 3. Encrypt project key with bot's public key
|
||||||
* 4. Send encrypted project key to backend and set bot status to active
|
* 4. Send encrypted project key to backend and set bot status to active
|
||||||
*/
|
*/
|
||||||
const toggleBotActivate = async () => {
|
const toggleBotActivate = async () => {
|
||||||
let botKey;
|
let botKey;
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace?._id) return;
|
if (!currentWorkspace?._id) return;
|
||||||
|
|
||||||
if (bot && wsKey) {
|
if (bot && wsKey) {
|
||||||
// case: there is a bot
|
// case: there is a bot
|
||||||
|
|
||||||
if (!bot.isActive) {
|
if (!bot.isActive) {
|
||||||
// bot is not active -> activate bot
|
// bot is not active -> activate bot
|
||||||
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||||
|
|
||||||
if (!PRIVATE_KEY) {
|
if (!PRIVATE_KEY) {
|
||||||
throw new Error("Private Key missing");
|
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return bot ? (
|
const WORKSPACE_KEY = decryptAssymmetric({
|
||||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
ciphertext: wsKey.encryptedKey,
|
||||||
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
|
nonce: wsKey.nonce,
|
||||||
<p className="text-gray-400 mb-8">
|
publicKey: wsKey.sender.publicKey,
|
||||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
|
privateKey: PRIVATE_KEY
|
||||||
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
|
});
|
||||||
read/decrypt your secret values.
|
|
||||||
</p>
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
<p className="text-gray-400 mb-8">
|
plaintext: WORKSPACE_KEY,
|
||||||
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
|
publicKey: bot.publicKey,
|
||||||
</p>
|
privateKey: PRIVATE_KEY
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
});
|
||||||
{(isAllowed) => (
|
|
||||||
<div className="w-max">
|
botKey = {
|
||||||
<Checkbox
|
encryptedKey: ciphertext,
|
||||||
className="data-[state=checked]:bg-primary"
|
nonce
|
||||||
id="autoCapitalization"
|
};
|
||||||
isChecked={!bot.isActive}
|
|
||||||
isDisabled={!isAllowed}
|
await updateBotActiveStatus({
|
||||||
onCheckedChange={async () => {
|
workspaceId: currentWorkspace._id,
|
||||||
await toggleBotActivate();
|
botKey,
|
||||||
}}
|
isActive: true,
|
||||||
>
|
botId: bot._id
|
||||||
End-to-end encryption enabled
|
});
|
||||||
</Checkbox>
|
} else {
|
||||||
</div>
|
// bot is active -> deactivate bot
|
||||||
)}
|
await updateBotActiveStatus({
|
||||||
</ProjectPermissionCan>
|
isActive: false,
|
||||||
</div>
|
botId: bot._id,
|
||||||
) : (
|
workspaceId: currentWorkspace._id
|
||||||
<div />
|
});
|
||||||
);
|
}
|
||||||
},
|
}
|
||||||
{
|
} catch (err) {
|
||||||
action: ProjectPermissionActions.Read,
|
console.error(err);
|
||||||
subject: ProjectPermissionSub.Settings
|
}
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
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 />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+102
-100
@@ -2,15 +2,15 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
|
import { Button, DeleteActionModal, UpgradePlanModal } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
|
||||||
import { useDeleteWsEnvironment } from "@app/hooks/api";
|
import { useDeleteWsEnvironment } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -18,111 +18,113 @@ import { AddEnvironmentModal } from "./AddEnvironmentModal";
|
|||||||
import { EnvironmentTable } from "./EnvironmentTable";
|
import { EnvironmentTable } from "./EnvironmentTable";
|
||||||
import { UpdateEnvironmentModal } from "./UpdateEnvironmentModal";
|
import { UpdateEnvironmentModal } from "./UpdateEnvironmentModal";
|
||||||
|
|
||||||
export const EnvironmentSection = withProjectPermission(
|
export const EnvironmentSection = () => {
|
||||||
() => {
|
const { createNotification } = useNotificationContext();
|
||||||
const { createNotification } = useNotificationContext();
|
const { subscription } = useSubscription();
|
||||||
const { subscription } = useSubscription();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const permision = useProjectPermission();
|
||||||
|
|
||||||
const deleteWsEnvironment = useDeleteWsEnvironment();
|
const deleteWsEnvironment = useDeleteWsEnvironment();
|
||||||
|
|
||||||
const isMoreEnvironmentsAllowed =
|
const isMoreEnvironmentsAllowed =
|
||||||
subscription?.environmentLimit && currentWorkspace?.environments
|
subscription?.environmentLimit && currentWorkspace?.environments
|
||||||
? currentWorkspace.environments.length < subscription.environmentLimit
|
? currentWorkspace.environments.length < subscription.environmentLimit
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"createEnv",
|
"createEnv",
|
||||||
"updateEnv",
|
"updateEnv",
|
||||||
"deleteEnv",
|
"deleteEnv",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const onEnvDeleteSubmit = async (environmentSlug: string) => {
|
const onEnvDeleteSubmit = async (environmentSlug: string) => {
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace?._id) return;
|
if (!currentWorkspace?._id) return;
|
||||||
|
|
||||||
await deleteWsEnvironment.mutateAsync({
|
await deleteWsEnvironment.mutateAsync({
|
||||||
workspaceID: currentWorkspace._id,
|
workspaceID: currentWorkspace._id,
|
||||||
environmentSlug
|
environmentSlug
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully deleted environment",
|
text: "Successfully deleted environment",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpClose("deleteEnv");
|
handlePopUpClose("deleteEnv");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to delete environment",
|
text: "Failed to delete environment",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex justify-between mb-8">
|
<div className="flex justify-between mb-8">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Environments</p>
|
||||||
<div>
|
<div>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.Environments}
|
a={ProjectPermissionSub.Environments}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMoreEnvironmentsAllowed) {
|
if (isMoreEnvironmentsAllowed) {
|
||||||
handlePopUpOpen("createEnv");
|
handlePopUpOpen("createEnv");
|
||||||
} else {
|
} else {
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Create environment
|
Create environment
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
<p className="text-gray-400 mb-8">
|
||||||
},
|
Choose which environments will show up in your dashboard like development, staging,
|
||||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Environments }
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+61
-65
@@ -5,7 +5,6 @@ import {
|
|||||||
} from "@app/components/utilities/cryptography/crypto";
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
|
||||||
import {
|
import {
|
||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
useGetWorkspaceIndexStatus,
|
useGetWorkspaceIndexStatus,
|
||||||
@@ -16,73 +15,70 @@ import {
|
|||||||
// TODO: add check so that this only shows up if user is
|
// TODO: add check so that this only shows up if user is
|
||||||
// an admin in the workspace
|
// an admin in the workspace
|
||||||
|
|
||||||
export const ProjectIndexSecretsSection = withProjectPermission(
|
export const ProjectIndexSecretsSection = () => {
|
||||||
() => {
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
|
||||||
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(
|
currentWorkspace?._id ?? ""
|
||||||
currentWorkspace?._id ?? ""
|
);
|
||||||
);
|
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
|
||||||
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
|
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
|
||||||
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
|
|
||||||
|
|
||||||
const onEnableBlindIndices = async () => {
|
const onEnableBlindIndices = async () => {
|
||||||
if (!currentWorkspace?._id) return;
|
if (!currentWorkspace?._id) return;
|
||||||
if (!encryptedSecrets) return;
|
if (!encryptedSecrets) return;
|
||||||
if (!latestFileKey) return;
|
if (!latestFileKey) return;
|
||||||
|
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
ciphertext: latestFileKey.encryptedKey,
|
ciphertext: latestFileKey.encryptedKey,
|
||||||
nonce: latestFileKey.nonce,
|
nonce: latestFileKey.nonce,
|
||||||
publicKey: latestFileKey.sender.publicKey,
|
publicKey: latestFileKey.sender.publicKey,
|
||||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
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) => {
|
return {
|
||||||
const secretName = decryptSymmetric({
|
secretName,
|
||||||
ciphertext: encryptedSecret.secretKeyCiphertext,
|
_id: encryptedSecret._id
|
||||||
iv: encryptedSecret.secretKeyIV,
|
};
|
||||||
tag: encryptedSecret.secretKeyTag,
|
});
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
await nameWorkspaceSecrets.mutateAsync({
|
||||||
secretName,
|
workspaceId: currentWorkspace._id,
|
||||||
_id: encryptedSecret._id
|
secretsToUpdate
|
||||||
};
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
await nameWorkspaceSecrets.mutateAsync({
|
return !isBlindIndexedLoading && !isBlindIndexed ? (
|
||||||
workspaceId: currentWorkspace._id,
|
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||||
secretsToUpdate
|
<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
|
||||||
return !isBlindIndexedLoading && !isBlindIndexed ? (
|
indexing.
|
||||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
</p>
|
||||||
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||||
<p className="text-gray-400 mb-8">
|
{(isAllowed) => (
|
||||||
Your project, created before the introduction of blind indexing, contains unindexed
|
<Button
|
||||||
secrets. To access individual secrets by name through the SDK and public API, please
|
onClick={onEnableBlindIndices}
|
||||||
enable blind indexing.
|
isDisabled={!isAllowed}
|
||||||
</p>
|
color="mineshaft"
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
size="sm"
|
||||||
{(isAllowed) => (
|
type="submit"
|
||||||
<Button
|
>
|
||||||
onClick={onEnableBlindIndices}
|
Enable Blind Indexing
|
||||||
isDisabled={!isAllowed}
|
</Button>
|
||||||
color="mineshaft"
|
)}
|
||||||
size="sm"
|
</ProjectPermissionCan>
|
||||||
type="submit"
|
</div>
|
||||||
>
|
) : (
|
||||||
Enable Blind Indexing
|
<div />
|
||||||
</Button>
|
);
|
||||||
)}
|
};
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div />
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Settings }
|
|
||||||
);
|
|
||||||
|
|||||||
+75
-74
@@ -2,10 +2,9 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { PermissionDeniedBanner, ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteWsTag } from "@app/hooks/api";
|
import { useDeleteWsTag } from "@app/hooks/api";
|
||||||
|
|
||||||
@@ -14,80 +13,82 @@ import { SecretTagsTable } from "./SecretTagsTable";
|
|||||||
|
|
||||||
type DeleteModalData = { name: string; id: string };
|
type DeleteModalData = { name: string; id: string };
|
||||||
|
|
||||||
export const SecretTagsSection = withProjectPermission(
|
export const SecretTagsSection = (): JSX.Element => {
|
||||||
(): JSX.Element => {
|
const { createNotification } = useNotificationContext();
|
||||||
const { createNotification } = useNotificationContext();
|
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
"CreateSecretTag",
|
||||||
"CreateSecretTag",
|
"deleteTagConfirmation"
|
||||||
"deleteTagConfirmation"
|
] as const);
|
||||||
] as const);
|
const permission = useProjectPermission();
|
||||||
|
|
||||||
const deleteWsTag = useDeleteWsTag();
|
const deleteWsTag = useDeleteWsTag();
|
||||||
|
|
||||||
const onDeleteApproved = async () => {
|
const onDeleteApproved = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteWsTag.mutateAsync({
|
await deleteWsTag.mutateAsync({
|
||||||
tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id
|
tagID: (popUp?.deleteTagConfirmation?.data as DeleteModalData)?.id
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully deleted tag",
|
text: "Successfully deleted tag",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpClose("deleteTagConfirmation");
|
handlePopUpClose("deleteTagConfirmation");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to delete the tag",
|
text: "Failed to delete the tag",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||||
<div className="flex justify-between mb-8">
|
<div className="flex justify-between mb-8">
|
||||||
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
|
<p className="mb-3 text-xl font-semibold">Secret Tags</p>
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Tags}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("x");
|
console.log("x");
|
||||||
handlePopUpOpen("CreateSecretTag");
|
handlePopUpOpen("CreateSecretTag");
|
||||||
console.log("x2");
|
console.log("x2");
|
||||||
}}
|
}}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Create tag
|
Create tag
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</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}
|
|
||||||
/>
|
|
||||||
</div>
|
</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
|
||||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Tags }
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user