mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 16:26:16 +00:00
Revert "Permission phase 2"
This commit is contained in:
@@ -56,10 +56,7 @@ describe("Secret expansion", () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const secret of secrets) {
|
await Promise.all(secrets.map((el) => createSecretV2(el)));
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await createSecretV2(secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandedSecret = await getSecretByNameV2({
|
const expandedSecret = await getSecretByNameV2({
|
||||||
environmentSlug: seedData1.environment.slug,
|
environmentSlug: seedData1.environment.slug,
|
||||||
@@ -126,10 +123,7 @@ describe("Secret expansion", () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const secret of secrets) {
|
await Promise.all(secrets.map((el) => createSecretV2(el)));
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await createSecretV2(secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandedSecret = await getSecretByNameV2({
|
const expandedSecret = await getSecretByNameV2({
|
||||||
environmentSlug: seedData1.environment.slug,
|
environmentSlug: seedData1.environment.slug,
|
||||||
@@ -196,11 +190,7 @@ describe("Secret expansion", () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const secret of secrets) {
|
await Promise.all(secrets.map((el) => createSecretV2(el)));
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await createSecretV2(secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretImportFromProdToDev = await createSecretImport({
|
const secretImportFromProdToDev = await createSecretImport({
|
||||||
environmentSlug: seedData1.environment.slug,
|
environmentSlug: seedData1.environment.slug,
|
||||||
workspaceId: projectId,
|
workspaceId: projectId,
|
||||||
@@ -285,11 +275,7 @@ describe("Secret expansion", () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const secret of secrets) {
|
await Promise.all(secrets.map((el) => createSecretV2(el)));
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await createSecretV2(secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretImportFromProdToDev = await createSecretImport({
|
const secretImportFromProdToDev = await createSecretImport({
|
||||||
environmentSlug: seedData1.environment.slug,
|
environmentSlug: seedData1.environment.slug,
|
||||||
workspaceId: projectId,
|
workspaceId: projectId,
|
||||||
|
|||||||
Generated
+266
-348
File diff suppressed because it is too large
Load Diff
@@ -4,40 +4,27 @@ import { TableName } from "../schemas";
|
|||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
const hasEncryptedSecret = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSecret");
|
|
||||||
const hasIdentifier = await knex.schema.hasColumn(TableName.SecretSharing, "identifier");
|
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
t.string("iv").nullable().alter();
|
t.string("iv").nullable().alter();
|
||||||
t.string("tag").nullable().alter();
|
t.string("tag").nullable().alter();
|
||||||
t.string("encryptedValue").nullable().alter();
|
t.string("encryptedValue").nullable().alter();
|
||||||
|
|
||||||
if (!hasEncryptedSecret) {
|
t.binary("encryptedSecret").nullable();
|
||||||
t.binary("encryptedSecret").nullable();
|
|
||||||
}
|
|
||||||
t.string("hashedHex").nullable().alter();
|
t.string("hashedHex").nullable().alter();
|
||||||
|
|
||||||
if (!hasIdentifier) {
|
t.string("identifier", 64).nullable();
|
||||||
t.string("identifier", 64).nullable();
|
t.unique("identifier");
|
||||||
t.unique("identifier");
|
t.index("identifier");
|
||||||
t.index("identifier");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
const hasEncryptedSecret = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSecret");
|
|
||||||
const hasIdentifier = await knex.schema.hasColumn(TableName.SecretSharing, "identifier");
|
|
||||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||||
if (hasEncryptedSecret) {
|
t.dropColumn("encryptedSecret");
|
||||||
t.dropColumn("encryptedSecret");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasIdentifier) {
|
t.dropColumn("identifier");
|
||||||
t.dropColumn("identifier");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,15 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||||
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||||
const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId");
|
|
||||||
|
|
||||||
// drop constraint if exists (won't exist if rolled back, see below)
|
// drop constraint if exists (won't exist if rolled back, see below)
|
||||||
await dropConstraintIfExists(TableName.KmsKey, "kms_keys_orgid_slug_unique", knex);
|
await dropConstraintIfExists(TableName.KmsKey, "kms_keys_orgid_slug_unique", knex);
|
||||||
|
|
||||||
// projectId for CMEK functionality
|
// projectId for CMEK functionality
|
||||||
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||||
if (!hasProjectId) {
|
table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
table.string("projectId").nullable().references("id").inTable(TableName.Project).onDelete("CASCADE");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasOrgId && hasSlug) {
|
if (hasOrgId) {
|
||||||
table.unique(["orgId", "projectId", "slug"]);
|
table.unique(["orgId", "projectId", "slug"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +30,6 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
if (await knex.schema.hasTable(TableName.KmsKey)) {
|
||||||
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||||
const hasName = await knex.schema.hasColumn(TableName.KmsKey, "name");
|
const hasName = await knex.schema.hasColumn(TableName.KmsKey, "name");
|
||||||
const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId");
|
|
||||||
|
|
||||||
// remove projectId for CMEK functionality
|
// remove projectId for CMEK functionality
|
||||||
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
await knex.schema.alterTable(TableName.KmsKey, (table) => {
|
||||||
@@ -44,9 +40,7 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
if (hasOrgId) {
|
if (hasOrgId) {
|
||||||
table.dropUnique(["orgId", "projectId", "slug"]);
|
table.dropUnique(["orgId", "projectId", "slug"]);
|
||||||
}
|
}
|
||||||
if (hasProjectId) {
|
table.dropColumn("projectId");
|
||||||
table.dropColumn("projectId");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
|
||||||
import { packRules, unpackRules } from "@casl/ability/extra";
|
|
||||||
import { Knex } from "knex";
|
|
||||||
|
|
||||||
import {
|
|
||||||
backfillPermissionV1SchemaToV2Schema,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
|
|
||||||
import { TableName } from "../schemas";
|
|
||||||
|
|
||||||
const CHUNK_SIZE = 1000;
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
|
||||||
const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version");
|
|
||||||
if (!hasVersion) {
|
|
||||||
await knex.schema.alterTable(TableName.ProjectRoles, (t) => {
|
|
||||||
t.integer("version").defaultTo(1).notNullable();
|
|
||||||
});
|
|
||||||
|
|
||||||
const docs = await knex(TableName.ProjectRoles).select("*");
|
|
||||||
const updatedDocs = docs
|
|
||||||
.filter((i) => {
|
|
||||||
const permissionString = JSON.stringify(i.permissions || []);
|
|
||||||
return (
|
|
||||||
!permissionString.includes(ProjectPermissionSub.SecretImports) &&
|
|
||||||
!permissionString.includes(ProjectPermissionSub.DynamicSecrets)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((el) => ({
|
|
||||||
...el,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore-error this is valid ts
|
|
||||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions))))
|
|
||||||
}));
|
|
||||||
if (updatedDocs.length) {
|
|
||||||
for (let i = 0; i < updatedDocs.length; i += CHUNK_SIZE) {
|
|
||||||
const chunk = updatedDocs.slice(i, i + CHUNK_SIZE);
|
|
||||||
await knex(TableName.ProjectRoles).insert(chunk).onConflict("id").merge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// secret permission is split into multiple ones like secrets, folders, imports and dynamic-secrets
|
|
||||||
// so we just find all the privileges with respective mapping and map it as needed
|
|
||||||
const identityPrivileges = await knex(TableName.IdentityProjectAdditionalPrivilege).select("*");
|
|
||||||
const updatedIdentityPrivilegesDocs = identityPrivileges
|
|
||||||
.filter((i) => {
|
|
||||||
const permissionString = JSON.stringify(i.permissions || []);
|
|
||||||
return (
|
|
||||||
!permissionString.includes(ProjectPermissionSub.SecretImports) &&
|
|
||||||
!permissionString.includes(ProjectPermissionSub.DynamicSecrets) &&
|
|
||||||
!permissionString.includes(ProjectPermissionSub.SecretFolders)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((el) => ({
|
|
||||||
...el,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore-error this is valid ts
|
|
||||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions))))
|
|
||||||
}));
|
|
||||||
if (updatedIdentityPrivilegesDocs.length) {
|
|
||||||
for (let i = 0; i < updatedIdentityPrivilegesDocs.length; i += CHUNK_SIZE) {
|
|
||||||
const chunk = updatedIdentityPrivilegesDocs.slice(i, i + CHUNK_SIZE);
|
|
||||||
await knex(TableName.IdentityProjectAdditionalPrivilege).insert(chunk).onConflict("id").merge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userPrivileges = await knex(TableName.ProjectUserAdditionalPrivilege).select("*");
|
|
||||||
const updatedUserPrivilegeDocs = userPrivileges
|
|
||||||
.filter((i) => {
|
|
||||||
const permissionString = JSON.stringify(i.permissions || []);
|
|
||||||
return (
|
|
||||||
!permissionString.includes(ProjectPermissionSub.SecretImports) &&
|
|
||||||
!permissionString.includes(ProjectPermissionSub.DynamicSecrets) &&
|
|
||||||
!permissionString.includes(ProjectPermissionSub.SecretFolders)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((el) => ({
|
|
||||||
...el,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore-error this is valid ts
|
|
||||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(unpackRules(el.permissions))))
|
|
||||||
}));
|
|
||||||
if (docs.length) {
|
|
||||||
for (let i = 0; i < updatedUserPrivilegeDocs.length; i += CHUNK_SIZE) {
|
|
||||||
const chunk = updatedUserPrivilegeDocs.slice(i, i + CHUNK_SIZE);
|
|
||||||
await knex(TableName.ProjectUserAdditionalPrivilege).insert(chunk).onConflict("id").merge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
|
||||||
const hasVersion = await knex.schema.hasColumn(TableName.ProjectRoles, "version");
|
|
||||||
if (hasVersion) {
|
|
||||||
await knex.schema.alterTable(TableName.ProjectRoles, (t) => {
|
|
||||||
t.dropColumn("version");
|
|
||||||
});
|
|
||||||
|
|
||||||
// permission change can be ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import ms from "ms";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
||||||
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
|
|
||||||
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
@@ -80,9 +79,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
...req.body,
|
...req.body,
|
||||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||||
isTemporary: false,
|
isTemporary: false,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
permissions: JSON.stringify(packRules(permission))
|
||||||
// @ts-ignore-error this is valid ts
|
|
||||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission)))
|
|
||||||
});
|
});
|
||||||
return { privilege };
|
return { privilege };
|
||||||
}
|
}
|
||||||
@@ -162,9 +159,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
...req.body,
|
...req.body,
|
||||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
permissions: JSON.stringify(packRules(permission))
|
||||||
// @ts-ignore-error this is valid ts
|
|
||||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission)))
|
|
||||||
});
|
});
|
||||||
return { privilege };
|
return { privilege };
|
||||||
}
|
}
|
||||||
@@ -249,11 +244,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
projectSlug: req.body.projectSlug,
|
projectSlug: req.body.projectSlug,
|
||||||
data: {
|
data: {
|
||||||
...updatedInfo,
|
...updatedInfo,
|
||||||
permissions: permission
|
permissions: permission ? JSON.stringify(packRules(permission)) : undefined
|
||||||
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore-error this is valid ts
|
|
||||||
JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(permission)))
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { privilege };
|
return { privilege };
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
import {
|
import { ProjectPermissionSchema } from "@app/ee/services/permission/project-permission";
|
||||||
backfillPermissionV1SchemaToV2Schema,
|
|
||||||
ProjectPermissionV1Schema
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -46,7 +43,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
||||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -64,7 +61,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
projectSlug: req.params.projectSlug,
|
projectSlug: req.params.projectSlug,
|
||||||
data: {
|
data: {
|
||||||
...req.body,
|
...req.body,
|
||||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions)))
|
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { role };
|
return { role };
|
||||||
@@ -106,7 +103,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
||||||
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -125,9 +122,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
roleId: req.params.roleId,
|
roleId: req.params.roleId,
|
||||||
data: {
|
data: {
|
||||||
...req.body,
|
...req.body,
|
||||||
permissions: req.body.permissions
|
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
||||||
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions)))
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { role };
|
return { role };
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import { packRules } from "@casl/ability/extra";
|
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas";
|
import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas";
|
||||||
import { backfillPermissionV1SchemaToV2Schema } from "@app/ee/services/permission/project-permission";
|
|
||||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
|
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types";
|
||||||
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ProjectSpecificPrivilegePermissionSchema } from "@app/server/routes/sanitizedSchemas";
|
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
|
export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -34,9 +31,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||||
permissions: ProjectSpecificPrivilegePermissionSchema.describe(
|
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||||
PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -54,17 +49,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
...req.body,
|
...req.body,
|
||||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||||
isTemporary: false,
|
isTemporary: false,
|
||||||
permissions: JSON.stringify(
|
permissions: JSON.stringify(req.body.permissions)
|
||||||
packRules(
|
|
||||||
backfillPermissionV1SchemaToV2Schema(
|
|
||||||
req.body.permissions.actions.map((action) => ({
|
|
||||||
action,
|
|
||||||
subject: req.body.permissions.subject,
|
|
||||||
conditions: req.body.permissions.conditions
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
return { privilege };
|
return { privilege };
|
||||||
}
|
}
|
||||||
@@ -90,9 +75,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||||
permissions: ProjectSpecificPrivilegePermissionSchema.describe(
|
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
||||||
PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions
|
|
||||||
),
|
|
||||||
temporaryMode: z
|
temporaryMode: z
|
||||||
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
|
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
|
||||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
|
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
|
||||||
@@ -121,17 +104,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
...req.body,
|
...req.body,
|
||||||
slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`,
|
slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`,
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
permissions: JSON.stringify(
|
permissions: JSON.stringify(req.body.permissions)
|
||||||
packRules(
|
|
||||||
backfillPermissionV1SchemaToV2Schema(
|
|
||||||
req.body.permissions.actions.map((action) => ({
|
|
||||||
action,
|
|
||||||
subject: req.body.permissions.subject,
|
|
||||||
conditions: req.body.permissions.conditions
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
return { privilege };
|
return { privilege };
|
||||||
}
|
}
|
||||||
@@ -158,9 +131,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
message: "Slug must be a valid slug"
|
message: "Slug must be a valid slug"
|
||||||
})
|
})
|
||||||
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
|
||||||
permissions: ProjectSpecificPrivilegePermissionSchema.describe(
|
permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||||
PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions
|
|
||||||
).optional(),
|
|
||||||
isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
|
isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
|
||||||
temporaryMode: z
|
temporaryMode: z
|
||||||
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
|
.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode)
|
||||||
@@ -189,19 +160,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
...req.body,
|
...req.body,
|
||||||
permissions: req.body.permissions
|
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined,
|
||||||
? JSON.stringify(
|
|
||||||
packRules(
|
|
||||||
backfillPermissionV1SchemaToV2Schema(
|
|
||||||
req.body.permissions.actions.map((action) => ({
|
|
||||||
action,
|
|
||||||
subject: req.body.permissions!.subject,
|
|
||||||
conditions: req.body.permissions!.conditions
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
privilegeId: req.params.privilegeId
|
privilegeId: req.params.privilegeId
|
||||||
});
|
});
|
||||||
return { privilege };
|
return { privilege };
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { registerProjectRoleRouter } from "./project-role-router";
|
|
||||||
|
|
||||||
export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
|
||||||
// org role starts with organization
|
|
||||||
await server.register(
|
|
||||||
async (projectRouter) => {
|
|
||||||
await projectRouter.register(registerProjectRoleRouter);
|
|
||||||
},
|
|
||||||
{ prefix: "/workspace" }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
import { packRules } from "@casl/ability/extra";
|
|
||||||
import slugify from "@sindresorhus/slugify";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
|
||||||
import { PROJECT_ROLE } from "@app/lib/api-docs";
|
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|
||||||
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
|
||||||
|
|
||||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
|
||||||
server.route({
|
|
||||||
method: "POST",
|
|
||||||
url: "/:projectSlug/roles",
|
|
||||||
config: {
|
|
||||||
rateLimit: writeLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
description: "Create a project role",
|
|
||||||
security: [
|
|
||||||
{
|
|
||||||
bearerAuth: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
params: z.object({
|
|
||||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
slug: z
|
|
||||||
.string()
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.refine(
|
|
||||||
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
|
||||||
)
|
|
||||||
.refine((v) => slugify(v) === v, {
|
|
||||||
message: "Slug must be a valid"
|
|
||||||
})
|
|
||||||
.describe(PROJECT_ROLE.CREATE.slug),
|
|
||||||
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
role: SanitizedRoleSchema
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const role = await server.services.projectRole.createRole({
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actor: req.permission.type,
|
|
||||||
projectSlug: req.params.projectSlug,
|
|
||||||
data: {
|
|
||||||
...req.body,
|
|
||||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { role };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "PATCH",
|
|
||||||
url: "/:projectSlug/roles/:roleId",
|
|
||||||
config: {
|
|
||||||
rateLimit: writeLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
description: "Update a project role",
|
|
||||||
security: [
|
|
||||||
{
|
|
||||||
bearerAuth: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
params: z.object({
|
|
||||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug),
|
|
||||||
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
|
||||||
}),
|
|
||||||
body: z.object({
|
|
||||||
slug: z
|
|
||||||
.string()
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.optional()
|
|
||||||
.describe(PROJECT_ROLE.UPDATE.slug)
|
|
||||||
.refine(
|
|
||||||
(val) =>
|
|
||||||
typeof val === "undefined" ||
|
|
||||||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
|
||||||
)
|
|
||||||
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
|
||||||
message: "Slug must be a valid"
|
|
||||||
}),
|
|
||||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
|
||||||
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
|
|
||||||
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
role: SanitizedRoleSchema
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const role = await server.services.projectRole.updateRole({
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actor: req.permission.type,
|
|
||||||
projectSlug: req.params.projectSlug,
|
|
||||||
roleId: req.params.roleId,
|
|
||||||
data: {
|
|
||||||
...req.body,
|
|
||||||
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { role };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "DELETE",
|
|
||||||
url: "/:projectSlug/roles/:roleId",
|
|
||||||
config: {
|
|
||||||
rateLimit: writeLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
description: "Delete a project role",
|
|
||||||
security: [
|
|
||||||
{
|
|
||||||
bearerAuth: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
params: z.object({
|
|
||||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug),
|
|
||||||
roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
role: SanitizedRoleSchema
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const role = await server.services.projectRole.deleteRole({
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actor: req.permission.type,
|
|
||||||
projectSlug: req.params.projectSlug,
|
|
||||||
roleId: req.params.roleId
|
|
||||||
});
|
|
||||||
return { role };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "GET",
|
|
||||||
url: "/:projectSlug/roles",
|
|
||||||
config: {
|
|
||||||
rateLimit: readLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
description: "List project role",
|
|
||||||
security: [
|
|
||||||
{
|
|
||||||
bearerAuth: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
params: z.object({
|
|
||||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug)
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
roles: ProjectRolesSchema.omit({ permissions: true }).array()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const roles = await server.services.projectRole.listRoles({
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actor: req.permission.type,
|
|
||||||
projectSlug: req.params.projectSlug
|
|
||||||
});
|
|
||||||
return { roles };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "GET",
|
|
||||||
url: "/:projectSlug/roles/slug/:roleSlug",
|
|
||||||
config: {
|
|
||||||
rateLimit: readLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
params: z.object({
|
|
||||||
projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
|
|
||||||
roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
role: SanitizedRoleSchema
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const role = await server.services.projectRole.getRoleBySlug({
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actor: req.permission.type,
|
|
||||||
projectSlug: req.params.projectSlug,
|
|
||||||
roleSlug: req.params.roleSlug
|
|
||||||
});
|
|
||||||
return { role };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "GET",
|
|
||||||
url: "/:projectId/permissions",
|
|
||||||
config: {
|
|
||||||
rateLimit: readLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
params: z.object({
|
|
||||||
projectId: z.string().trim()
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
data: z.object({
|
|
||||||
membership: ProjectMembershipsSchema.extend({
|
|
||||||
roles: z
|
|
||||||
.object({
|
|
||||||
role: z.string()
|
|
||||||
})
|
|
||||||
.array()
|
|
||||||
}),
|
|
||||||
permissions: z.any().array()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const { permissions, membership } = await server.services.projectRole.getUserPermission(
|
|
||||||
req.permission.id,
|
|
||||||
req.params.projectId,
|
|
||||||
req.permission.authMethod,
|
|
||||||
req.permission.orgId
|
|
||||||
);
|
|
||||||
|
|
||||||
return { data: { permissions, membership } };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -14,7 +14,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const accessApprovalPolicyFindQuery = async (
|
const accessApprovalPolicyFindQuery = async (
|
||||||
tx: Knex,
|
tx: Knex,
|
||||||
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
|
filter: TFindFilter<TAccessApprovalPolicies>,
|
||||||
customFilter?: {
|
customFilter?: {
|
||||||
policyId?: string;
|
policyId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
|
import { TIsApproversValid } from "./access-approval-policy-types";
|
||||||
|
|
||||||
|
export const isApproversValid = async ({
|
||||||
|
userIds,
|
||||||
|
projectId,
|
||||||
|
orgId,
|
||||||
|
envSlug,
|
||||||
|
actorAuthMethod,
|
||||||
|
secretPath,
|
||||||
|
permissionService
|
||||||
|
}: TIsApproversValid) => {
|
||||||
|
try {
|
||||||
|
for await (const userId of userIds) {
|
||||||
|
const { permission: approverPermission } = await permissionService.getProjectPermission(
|
||||||
|
ActorType.USER,
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(approverPermission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
|||||||
import { TGroupDALFactory } from "../group/group-dal";
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
|
||||||
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
|
||||||
|
import { isApproversValid } from "./access-approval-policy-fns";
|
||||||
import {
|
import {
|
||||||
ApproverType,
|
ApproverType,
|
||||||
TCreateAccessApprovalPolicy,
|
TCreateAccessApprovalPolicy,
|
||||||
@@ -131,6 +132,22 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
.map((user) => user.id);
|
.map((user) => user.id);
|
||||||
verifyAllApprovers.push(...verifyGroupApprovers);
|
verifyAllApprovers.push(...verifyGroupApprovers);
|
||||||
|
|
||||||
|
const approversValid = await isApproversValid({
|
||||||
|
projectId: project.id,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
envSlug: environment,
|
||||||
|
secretPath,
|
||||||
|
actorAuthMethod,
|
||||||
|
permissionService,
|
||||||
|
userIds: verifyAllApprovers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!approversValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "One or more approvers doesn't have access to be specified secret path"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
|
||||||
const doc = await accessApprovalPolicyDAL.create(
|
const doc = await accessApprovalPolicyDAL.create(
|
||||||
{
|
{
|
||||||
@@ -272,6 +289,22 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
userApproverIds = userApproverIds.concat(approverUsers.map((user) => user.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const approversValid = await isApproversValid({
|
||||||
|
projectId: accessApprovalPolicy.projectId,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
envSlug: accessApprovalPolicy.environment.slug,
|
||||||
|
secretPath: doc.secretPath!,
|
||||||
|
actorAuthMethod,
|
||||||
|
permissionService,
|
||||||
|
userIds: userApproverIds
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!approversValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "One or more approvers doesn't have access to be specified secret path"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
userApproverIds.map((userId) => ({
|
userApproverIds.map((userId) => ({
|
||||||
approverUserId: userId,
|
approverUserId: userId,
|
||||||
@@ -282,6 +315,41 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (groupApprovers) {
|
if (groupApprovers) {
|
||||||
|
const usersPromises: Promise<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
email: string | null | undefined;
|
||||||
|
username: string;
|
||||||
|
firstName: string | null | undefined;
|
||||||
|
lastName: string | null | undefined;
|
||||||
|
isPartOfGroup: boolean;
|
||||||
|
}[]
|
||||||
|
>[] = [];
|
||||||
|
|
||||||
|
for (const groupId of groupApprovers) {
|
||||||
|
usersPromises.push(groupDAL.findAllGroupPossibleMembers({ orgId: actorOrgId, groupId, offset: 0 }));
|
||||||
|
}
|
||||||
|
const verifyGroupApprovers = (await Promise.all(usersPromises))
|
||||||
|
.flat()
|
||||||
|
.filter((user) => user.isPartOfGroup)
|
||||||
|
.map((user) => user.id);
|
||||||
|
|
||||||
|
const approversValid = await isApproversValid({
|
||||||
|
projectId: accessApprovalPolicy.projectId,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
envSlug: accessApprovalPolicy.environment.slug,
|
||||||
|
secretPath: doc.secretPath!,
|
||||||
|
actorAuthMethod,
|
||||||
|
permissionService,
|
||||||
|
userIds: verifyGroupApprovers
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!approversValid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "One or more approvers doesn't have access to be specified secret path"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await accessApprovalPolicyApproverDAL.insertMany(
|
await accessApprovalPolicyApproverDAL.insertMany(
|
||||||
groupApprovers.map((groupId) => ({
|
groupApprovers.map((groupId) => ({
|
||||||
approverGroupId: groupId,
|
approverGroupId: groupId,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
|
|||||||
|
|
||||||
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal";
|
||||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||||
|
import { isApproversValid } from "../access-approval-policy/access-approval-policy-fns";
|
||||||
import { TGroupDALFactory } from "../group/group-dal";
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
@@ -77,6 +78,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
accessApprovalRequestDAL,
|
accessApprovalRequestDAL,
|
||||||
accessApprovalRequestReviewerDAL,
|
accessApprovalRequestReviewerDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
accessApprovalPolicyDAL,
|
accessApprovalPolicyDAL,
|
||||||
accessApprovalPolicyApproverDAL,
|
accessApprovalPolicyApproverDAL,
|
||||||
additionalPrivilegeDAL,
|
additionalPrivilegeDAL,
|
||||||
@@ -321,6 +323,22 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" });
|
throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id);
|
||||||
|
|
||||||
|
const approversValid = await isApproversValid({
|
||||||
|
projectId: accessApprovalRequest.projectId,
|
||||||
|
orgId: actorOrgId,
|
||||||
|
envSlug: accessApprovalRequest.environment,
|
||||||
|
secretPath: accessApprovalRequest.policy.secretPath!,
|
||||||
|
actorAuthMethod,
|
||||||
|
permissionService,
|
||||||
|
userIds: [reviewerProjectMembership.userId]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!approversValid) {
|
||||||
|
throw new ForbiddenRequestError({ message: "You don't have access to approve this request" });
|
||||||
|
}
|
||||||
|
|
||||||
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
|
||||||
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
|
||||||
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import ms from "ms";
|
|||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
@@ -75,8 +72,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
@@ -148,8 +145,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
ProjectPermissionActions.Edit,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
@@ -222,8 +219,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
ProjectPermissionActions.Delete,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
@@ -287,8 +284,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
@@ -323,8 +320,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
|||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
@@ -80,8 +77,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
ProjectPermissionActions.Create,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
@@ -149,8 +146,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
ProjectPermissionActions.Edit,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
@@ -228,8 +225,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
ProjectPermissionActions.Edit,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
@@ -285,12 +282,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionActions.Edit,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
@@ -335,8 +328,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
// verify user has access to each env in request
|
// verify user has access to each env in request
|
||||||
environmentSlugs.forEach((environmentSlug) =>
|
environmentSlugs.forEach((environmentSlug) =>
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -371,8 +364,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
@@ -417,8 +410,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
@@ -459,8 +452,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
// verify user has access to each env in request
|
// verify user has access to each env in request
|
||||||
environmentSlugs.forEach((environmentSlug) =>
|
environmentSlugs.forEach((environmentSlug) =>
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.Secrets, { environment: environmentSlug, secretPath: path })
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -1,10 +1,10 @@
|
|||||||
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -32,6 +32,16 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
|||||||
typeof identityProjectAdditionalPrivilegeServiceFactory
|
typeof identityProjectAdditionalPrivilegeServiceFactory
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// TODO(akhilmhdh): move this to more centralized
|
||||||
|
export const UnpackedPermissionSchema = z.object({
|
||||||
|
subject: z
|
||||||
|
.union([z.string().min(1), z.string().array()])
|
||||||
|
.transform((el) => (typeof el !== "string" ? el[0] : el))
|
||||||
|
.optional(),
|
||||||
|
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
|
||||||
|
conditions: z.unknown().optional()
|
||||||
|
});
|
||||||
|
|
||||||
const unpackPermissions = (permissions: unknown) =>
|
const unpackPermissions = (permissions: unknown) =>
|
||||||
UnpackedPermissionSchema.array().parse(
|
UnpackedPermissionSchema.array().parse(
|
||||||
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||||
@@ -193,6 +203,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
...additionalPrivilege,
|
...additionalPrivilege,
|
||||||
|
|
||||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -313,6 +324,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
return identityPrivileges.map((el) => ({
|
return identityPrivileges.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
|
|
||||||
permissions: unpackPermissions(el.permissions)
|
permissions: unpackPermissions(el.permissions)
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const permissionServiceFactory = ({
|
|||||||
throw new NotFoundError({ name: "OrgRoleInvalid", message: "Organization role not found" });
|
throw new NotFoundError({ name: "OrgRoleInvalid", message: "Organization role not found" });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.reduce((prev, curr) => prev.concat(curr), []);
|
.reduce((curr, prev) => prev.concat(curr), []);
|
||||||
|
|
||||||
return createMongoAbility<OrgPermissionSet>(rules, {
|
return createMongoAbility<OrgPermissionSet>(rules, {
|
||||||
conditionsMatcher
|
conditionsMatcher
|
||||||
@@ -98,7 +98,7 @@ export const permissionServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.reduce((prev, curr) => prev.concat(curr), []);
|
.reduce((curr, prev) => prev.concat(curr), []);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export enum PermissionConditionOperators {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PermissionConditionSchema = {
|
export const PermissionConditionSchema = {
|
||||||
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),
|
[PermissionConditionOperators.$IN]: z.string().min(1).array(),
|
||||||
[PermissionConditionOperators.$ALL]: z.string().trim().min(1).array(),
|
[PermissionConditionOperators.$ALL]: z.string().min(1).array(),
|
||||||
[PermissionConditionOperators.$REGEX]: z
|
[PermissionConditionOperators.$REGEX]: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
import { conditionsMatcher } from "@app/lib/casl";
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
||||||
|
|
||||||
@@ -22,14 +23,6 @@ export enum ProjectPermissionCmekActions {
|
|||||||
Decrypt = "decrypt"
|
Decrypt = "decrypt"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionDynamicSecretActions {
|
|
||||||
ReadRootCredential = "read-root-credential",
|
|
||||||
CreateRootCredential = "create-root-credential",
|
|
||||||
EditRootCredential = "edit-root-credential",
|
|
||||||
DeleteRootCredential = "delete-root-credential",
|
|
||||||
Lease = "lease"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
@@ -45,8 +38,6 @@ export enum ProjectPermissionSub {
|
|||||||
Project = "workspace",
|
Project = "workspace",
|
||||||
Secrets = "secrets",
|
Secrets = "secrets",
|
||||||
SecretFolders = "secret-folders",
|
SecretFolders = "secret-folders",
|
||||||
SecretImports = "secret-imports",
|
|
||||||
DynamicSecrets = "dynamic-secrets",
|
|
||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
@@ -63,8 +54,19 @@ export enum ProjectPermissionSub {
|
|||||||
export type SecretSubjectFields = {
|
export type SecretSubjectFields = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
secretName?: string;
|
// secretName: string;
|
||||||
secretTags?: string[];
|
// secretTags: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CaslSecretsV2SubjectKnexMapper = (field: string) => {
|
||||||
|
switch (field) {
|
||||||
|
case "secretName":
|
||||||
|
return `${TableName.SecretV2}.key`;
|
||||||
|
case "secretTags":
|
||||||
|
return `${TableName.SecretTag}.slug`;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SecretFolderSubjectFields = {
|
export type SecretFolderSubjectFields = {
|
||||||
@@ -72,16 +74,6 @@ export type SecretFolderSubjectFields = {
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DynamicSecretSubjectFields = {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SecretImportSubjectFields = {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
@@ -94,20 +86,6 @@ export type ProjectPermissionSet =
|
|||||||
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
|
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
| [
|
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
(
|
|
||||||
| ProjectPermissionSub.DynamicSecrets
|
|
||||||
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
| [
|
|
||||||
ProjectPermissionActions,
|
|
||||||
(
|
|
||||||
| ProjectPermissionSub.SecretImports
|
|
||||||
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SecretImportSubjectFields)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||||
@@ -142,9 +120,7 @@ const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTI
|
|||||||
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
|
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
|
||||||
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
|
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
|
||||||
|
|
||||||
// akhilmhdh: don't modify this for v2
|
const SecretConditionSchema = z
|
||||||
// if you want to update create a new schema
|
|
||||||
const SecretConditionV1Schema = z
|
|
||||||
.object({
|
.object({
|
||||||
environment: z.union([
|
environment: z.union([
|
||||||
z.string(),
|
z.string(),
|
||||||
@@ -170,50 +146,16 @@ const SecretConditionV1Schema = z
|
|||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
const SecretConditionV2Schema = z
|
export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
|
||||||
.object({
|
z.object({
|
||||||
environment: z.union([
|
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||||
z.string(),
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
z
|
"Describe what action an entity can take."
|
||||||
.object({
|
),
|
||||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
conditions: SecretConditionSchema.describe(
|
||||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
).optional()
|
||||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
}),
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
]),
|
|
||||||
secretPath: z.union([
|
|
||||||
z.string(),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
|
||||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
|
||||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
|
||||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
]),
|
|
||||||
secretName: z.union([
|
|
||||||
z.string(),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
|
||||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
|
||||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
|
||||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
]),
|
|
||||||
secretTags: z
|
|
||||||
.object({
|
|
||||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
})
|
|
||||||
.partial();
|
|
||||||
|
|
||||||
const GeneralPermissionSchema = [
|
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
@@ -317,7 +259,7 @@ const GeneralPermissionSchema = [
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to. "),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
@@ -346,78 +288,18 @@ const GeneralPermissionSchema = [
|
|||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
z.object({
|
|
||||||
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
|
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
|
|
||||||
"Describe what action an entity can take."
|
|
||||||
)
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
|
|
||||||
z.object({
|
|
||||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
|
||||||
"Describe what action an entity can take."
|
|
||||||
),
|
|
||||||
conditions: SecretConditionV1Schema.describe(
|
|
||||||
"When specified, only matching conditions will be allowed to access given resource."
|
|
||||||
).optional()
|
|
||||||
}),
|
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
|
||||||
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
...GeneralPermissionSchema
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
)
|
||||||
conditions: SecretConditionV2Schema.describe(
|
})
|
||||||
"When specified, only matching conditions will be allowed to access given resource."
|
|
||||||
).optional()
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
|
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
|
||||||
"Describe what action an entity can take."
|
|
||||||
),
|
|
||||||
conditions: SecretConditionV1Schema.describe(
|
|
||||||
"When specified, only matching conditions will be allowed to access given resource."
|
|
||||||
).optional()
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
subject: z.literal(ProjectPermissionSub.SecretImports).describe("The entity this permission pertains to."),
|
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
|
||||||
"Describe what action an entity can take."
|
|
||||||
),
|
|
||||||
conditions: SecretConditionV1Schema.describe(
|
|
||||||
"When specified, only matching conditions will be allowed to access given resource."
|
|
||||||
).optional()
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
subject: z.literal(ProjectPermissionSub.DynamicSecrets).describe("The entity this permission pertains to."),
|
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
|
||||||
"Describe what action an entity can take."
|
|
||||||
),
|
|
||||||
conditions: SecretConditionV1Schema.describe(
|
|
||||||
"When specified, only matching conditions will be allowed to access given resource."
|
|
||||||
).optional()
|
|
||||||
}),
|
|
||||||
...GeneralPermissionSchema
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const buildAdminPermissionRules = () => {
|
const buildAdminPermissionRules = () => {
|
||||||
@@ -426,8 +308,6 @@ const buildAdminPermissionRules = () => {
|
|||||||
// Admins get full access to everything
|
// Admins get full access to everything
|
||||||
[
|
[
|
||||||
ProjectPermissionSub.Secrets,
|
ProjectPermissionSub.Secrets,
|
||||||
ProjectPermissionSub.SecretFolders,
|
|
||||||
ProjectPermissionSub.SecretImports,
|
|
||||||
ProjectPermissionSub.SecretApproval,
|
ProjectPermissionSub.SecretApproval,
|
||||||
ProjectPermissionSub.SecretRotation,
|
ProjectPermissionSub.SecretRotation,
|
||||||
ProjectPermissionSub.Member,
|
ProjectPermissionSub.Member,
|
||||||
@@ -459,17 +339,6 @@ const buildAdminPermissionRules = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
can(
|
|
||||||
[
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
|
||||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
|
||||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease
|
|
||||||
],
|
|
||||||
ProjectPermissionSub.DynamicSecrets
|
|
||||||
);
|
|
||||||
|
|
||||||
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
|
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
|
||||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||||
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
|
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
|
||||||
@@ -501,34 +370,6 @@ const buildMemberPermissionRules = () => {
|
|||||||
],
|
],
|
||||||
ProjectPermissionSub.Secrets
|
ProjectPermissionSub.Secrets
|
||||||
);
|
);
|
||||||
can(
|
|
||||||
[
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionActions.Delete
|
|
||||||
],
|
|
||||||
ProjectPermissionSub.SecretFolders
|
|
||||||
);
|
|
||||||
can(
|
|
||||||
[
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
|
||||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
|
||||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease
|
|
||||||
],
|
|
||||||
ProjectPermissionSub.DynamicSecrets
|
|
||||||
);
|
|
||||||
can(
|
|
||||||
[
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionActions.Delete
|
|
||||||
],
|
|
||||||
ProjectPermissionSub.SecretImports
|
|
||||||
);
|
|
||||||
|
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
||||||
@@ -652,9 +493,6 @@ const buildViewerPermissionRules = () => {
|
|||||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
|
||||||
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||||
@@ -757,52 +595,17 @@ export const isAtLeastAsPrivilegedWorkspace = (
|
|||||||
};
|
};
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
export const backfillPermissionV1SchemaToV2Schema = (data: z.infer<typeof ProjectPermissionV1Schema>[]) => {
|
export const SecretV2SubjectFieldMapper = (arg: string) => {
|
||||||
const formattedData = UnpackedPermissionSchema.array().parse(data);
|
switch (arg) {
|
||||||
const secretSubjects = formattedData.filter((el) => el.subject === ProjectPermissionSub.Secrets);
|
case "environment":
|
||||||
|
return null;
|
||||||
// this means the folder permission as readonly is set
|
case "secretPath":
|
||||||
const hasReadOnlyFolder = formattedData.filter((el) => el.subject === ProjectPermissionSub.SecretFolders);
|
return null;
|
||||||
const secretImportPolicies = secretSubjects.map(({ subject, ...el }) => ({
|
case "secretName":
|
||||||
...el,
|
return `${TableName.SecretV2}.key`;
|
||||||
subject: ProjectPermissionSub.SecretImports as const
|
case "secretTags":
|
||||||
}));
|
return `${TableName.SecretTag}.slug`;
|
||||||
|
default:
|
||||||
const secretFolderPolicies = secretSubjects.map(({ subject, ...el }) => ({
|
throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` });
|
||||||
...el,
|
}
|
||||||
subject: ProjectPermissionSub.SecretFolders
|
|
||||||
}));
|
|
||||||
|
|
||||||
const dynamicSecretPolicies = secretSubjects.map(({ subject, ...el }) => {
|
|
||||||
const action = el.action.map((e) => {
|
|
||||||
switch (e) {
|
|
||||||
case ProjectPermissionActions.Edit:
|
|
||||||
return ProjectPermissionDynamicSecretActions.EditRootCredential;
|
|
||||||
case ProjectPermissionActions.Create:
|
|
||||||
return ProjectPermissionDynamicSecretActions.CreateRootCredential;
|
|
||||||
case ProjectPermissionActions.Delete:
|
|
||||||
return ProjectPermissionDynamicSecretActions.DeleteRootCredential;
|
|
||||||
case ProjectPermissionActions.Read:
|
|
||||||
return ProjectPermissionDynamicSecretActions.ReadRootCredential;
|
|
||||||
default:
|
|
||||||
return ProjectPermissionDynamicSecretActions.ReadRootCredential;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...el,
|
|
||||||
action: el.action.includes(ProjectPermissionActions.Edit)
|
|
||||||
? [...action, ProjectPermissionDynamicSecretActions.Lease]
|
|
||||||
: action,
|
|
||||||
subject: ProjectPermissionSub.DynamicSecrets
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return formattedData.concat(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore-error this is valid ts
|
|
||||||
secretImportPolicies,
|
|
||||||
dynamicSecretPolicies,
|
|
||||||
hasReadOnlyFolder.length ? [] : secretFolderPolicies
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-38
@@ -1,13 +1,11 @@
|
|||||||
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
|
||||||
import {
|
import {
|
||||||
ProjectUserAdditionalPrivilegeTemporaryMode,
|
ProjectUserAdditionalPrivilegeTemporaryMode,
|
||||||
@@ -28,11 +26,6 @@ export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType<
|
|||||||
typeof projectUserAdditionalPrivilegeServiceFactory
|
typeof projectUserAdditionalPrivilegeServiceFactory
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const unpackPermissions = (permissions: unknown) =>
|
|
||||||
UnpackedPermissionSchema.array().parse(
|
|
||||||
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const projectUserAdditionalPrivilegeServiceFactory = ({
|
export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||||
projectUserAdditionalPrivilegeDAL,
|
projectUserAdditionalPrivilegeDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
@@ -74,10 +67,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
slug,
|
slug,
|
||||||
permissions: customPermission
|
permissions: customPermission
|
||||||
});
|
});
|
||||||
return {
|
return additionalPrivilege;
|
||||||
...additionalPrivilege,
|
|
||||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
|
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
|
||||||
@@ -92,10 +82,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
||||||
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||||
});
|
});
|
||||||
return {
|
return additionalPrivilege;
|
||||||
...additionalPrivilege,
|
|
||||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateById = async ({
|
const updateById = async ({
|
||||||
@@ -144,11 +131,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||||
});
|
});
|
||||||
|
return additionalPrivilege;
|
||||||
return {
|
|
||||||
...additionalPrivilege,
|
|
||||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
|
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, {
|
||||||
@@ -159,10 +142,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
temporaryRange: null,
|
temporaryRange: null,
|
||||||
temporaryMode: null
|
temporaryMode: null
|
||||||
});
|
});
|
||||||
return {
|
return additionalPrivilege;
|
||||||
...additionalPrivilege,
|
|
||||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
|
const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => {
|
||||||
@@ -185,10 +165,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
|
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
|
||||||
return {
|
return deletedPrivilege;
|
||||||
...deletedPrivilege,
|
|
||||||
permissions: unpackPermissions(deletedPrivilege.permissions)
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPrivilegeDetailsById = async ({
|
const getPrivilegeDetailsById = async ({
|
||||||
@@ -216,10 +193,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||||
|
|
||||||
return {
|
return userPrivilege;
|
||||||
...userPrivilege,
|
|
||||||
permissions: unpackPermissions(userPrivilege.permissions)
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const listPrivileges = async ({
|
const listPrivileges = async ({
|
||||||
@@ -245,10 +219,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
|||||||
userId: projectMembership.userId,
|
userId: projectMembership.userId,
|
||||||
projectId: projectMembership.projectId
|
projectId: projectMembership.projectId
|
||||||
});
|
});
|
||||||
return userPrivileges.map((el) => ({
|
return userPrivileges;
|
||||||
...el,
|
|
||||||
permissions: unpackPermissions(el.permissions)
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const secretApprovalPolicyFindQuery = (
|
const secretApprovalPolicyFindQuery = (
|
||||||
tx: Knex,
|
tx: Knex,
|
||||||
filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>,
|
filter: TFindFilter<TSecretApprovalPolicies>,
|
||||||
customFilter?: {
|
customFilter?: {
|
||||||
sapId?: string;
|
sapId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -344,8 +344,17 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
environment,
|
environment,
|
||||||
secretPath
|
secretPath
|
||||||
}: TGetBoardSapDTO) => {
|
}: TGetBoardSapDTO) => {
|
||||||
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
|
||||||
|
);
|
||||||
return getSecretApprovalPolicy(projectId, environment, secretPath);
|
return getSecretApprovalPolicy(projectId, environment, secretPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import {
|
|||||||
fnSecretBulkDelete as fnSecretV2BridgeBulkDelete,
|
fnSecretBulkDelete as fnSecretV2BridgeBulkDelete,
|
||||||
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
||||||
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
||||||
getAllSecretReferences as getAllSecretReferencesV2Bridge
|
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
|
||||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||||
@@ -523,11 +523,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||||
key: el.key,
|
key: el.key,
|
||||||
references: el.encryptedValue
|
references: el.encryptedValue
|
||||||
? getAllSecretReferencesV2Bridge(
|
? getAllNestedSecretReferencesV2Bridge(
|
||||||
secretManagerDecryptor({
|
secretManagerDecryptor({
|
||||||
cipherTextBlob: el.encryptedValue
|
cipherTextBlob: el.encryptedValue
|
||||||
}).toString()
|
}).toString()
|
||||||
).nestedReferences
|
)
|
||||||
: [],
|
: [],
|
||||||
type: SecretType.Shared
|
type: SecretType.Shared
|
||||||
})),
|
})),
|
||||||
@@ -547,11 +547,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
? {
|
? {
|
||||||
encryptedValue: el.encryptedValue as Buffer,
|
encryptedValue: el.encryptedValue as Buffer,
|
||||||
references: el.encryptedValue
|
references: el.encryptedValue
|
||||||
? getAllSecretReferencesV2Bridge(
|
? getAllNestedSecretReferencesV2Bridge(
|
||||||
secretManagerDecryptor({
|
secretManagerDecryptor({
|
||||||
cipherTextBlob: el.encryptedValue
|
cipherTextBlob: el.encryptedValue
|
||||||
}).toString()
|
}).toString()
|
||||||
).nestedReferences
|
)
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
@@ -1125,6 +1125,10 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -1288,23 +1292,6 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const tagIds = unique(Object.values(commitTagIds).flat());
|
const tagIds = unique(Object.values(commitTagIds).flat());
|
||||||
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
|
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
|
||||||
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "Tag not found" });
|
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "Tag not found" });
|
||||||
const tagsGroupById = groupBy(tags, (i) => i.id);
|
|
||||||
|
|
||||||
commits.forEach((commit) => {
|
|
||||||
let action = ProjectPermissionActions.Create;
|
|
||||||
if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit;
|
|
||||||
if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete;
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
action,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: commit.key,
|
|
||||||
secretTags: commitTagIds?.[commit.key]?.map((secretTagId) => tagsGroupById[secretTagId][0].slug)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
|
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||||
const doc = await secretApprovalRequestDAL.create(
|
const doc = await secretApprovalRequestDAL.create(
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
|
|||||||
import {
|
import {
|
||||||
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
||||||
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
||||||
getAllSecretReferences
|
getAllNestedSecretReferences,
|
||||||
|
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
|
||||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||||
@@ -252,12 +253,11 @@ export const secretReplicationServiceFactory = ({
|
|||||||
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
||||||
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
||||||
const sourceImportedSecrets = await fnSecretsV2FromImports({
|
const sourceImportedSecrets = await fnSecretsV2FromImports({
|
||||||
secretImports: sourceSecretImports,
|
allowedImports: sourceSecretImports,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||||
hasSecretAccess: () => true
|
|
||||||
});
|
});
|
||||||
// secrets that gets replicated across imports
|
// secrets that gets replicated across imports
|
||||||
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
|
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
|
||||||
@@ -416,7 +416,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
encryptedValue: doc.encryptedValue,
|
encryptedValue: doc.encryptedValue,
|
||||||
encryptedComment: doc.encryptedComment,
|
encryptedComment: doc.encryptedComment,
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||||
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
|
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -442,7 +442,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
encryptedValue: doc.encryptedValue as Buffer,
|
encryptedValue: doc.encryptedValue as Buffer,
|
||||||
encryptedComment: doc.encryptedComment,
|
encryptedComment: doc.encryptedComment,
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||||
references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : []
|
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -687,7 +687,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
secretCommentTag: doc.secretCommentTag,
|
secretCommentTag: doc.secretCommentTag,
|
||||||
secretCommentCiphertext: doc.secretCommentCiphertext,
|
secretCommentCiphertext: doc.secretCommentCiphertext,
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||||
references: getAllSecretReferences(doc.secretValue).nestedReferences
|
references: getAllNestedSecretReferences(doc.secretValue)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -723,7 +723,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
secretCommentTag: doc.secretCommentTag,
|
secretCommentTag: doc.secretCommentTag,
|
||||||
secretCommentCiphertext: doc.secretCommentCiphertext,
|
secretCommentCiphertext: doc.secretCommentCiphertext,
|
||||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||||
references: getAllSecretReferences(doc.secretValue).nestedReferences
|
references: getAllNestedSecretReferences(doc.secretValue)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import Ajv from "ajv";
|
import Ajv from "ajv";
|
||||||
|
|
||||||
import { ProjectVersion, TableName } from "@app/db/schemas";
|
import { ProjectVersion } from "@app/db/schemas";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
@@ -99,14 +99,13 @@ export const secretRotationServiceFactory = ({
|
|||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
const shouldUseBridge = project.version === ProjectVersion.V3;
|
const shouldUseBridge = project.version === ProjectVersion.V3;
|
||||||
|
|
||||||
if (shouldUseBridge) {
|
if (shouldUseBridge) {
|
||||||
const selectedSecrets = await secretV2BridgeDAL.find({
|
const selectedSecrets = await secretV2BridgeDAL.find({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
$in: { [`${TableName.SecretV2}.id` as "id"]: Object.values(outputs) }
|
$in: { id: Object.values(outputs) }
|
||||||
});
|
});
|
||||||
if (selectedSecrets.length !== Object.values(outputs).length)
|
if (selectedSecrets.length !== Object.values(outputs).length)
|
||||||
throw new NotFoundError({ message: "Secrets not found" });
|
throw new NotFoundError({ message: "Secrets not found" });
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { AnyAbility, ExtractSubjectType } from "@casl/ability";
|
||||||
|
import { AbilityQuery, rulesToQuery } from "@casl/ability/extra";
|
||||||
|
import { Tables } from "knex/types/tables";
|
||||||
|
|
||||||
|
import { BadRequestError, UnauthorizedError } from "../errors";
|
||||||
|
import { TKnexDynamicOperator } from "../knex/dynamic";
|
||||||
|
|
||||||
|
type TBuildKnexQueryFromCaslDTO<K extends AnyAbility> = {
|
||||||
|
ability: K;
|
||||||
|
subject: ExtractSubjectType<Parameters<K["rulesFor"]>[1]>;
|
||||||
|
action: Parameters<K["rulesFor"]>[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildKnexQueryFromCaslOperators = <K extends AnyAbility>({
|
||||||
|
ability,
|
||||||
|
subject,
|
||||||
|
action
|
||||||
|
}: TBuildKnexQueryFromCaslDTO<K>) => {
|
||||||
|
const query = rulesToQuery(ability, action, subject, (rule) => {
|
||||||
|
if (!rule.ast) throw new Error("Ast not defined");
|
||||||
|
return rule.ast;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query === null) throw new UnauthorizedError({ message: `You don't have permission to do ${action} ${subject}` });
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TFieldMapper<T extends keyof Tables> = {
|
||||||
|
[K in T]: `${K}.${Exclude<keyof Tables[K]["base"], symbol>}`;
|
||||||
|
}[T];
|
||||||
|
|
||||||
|
type TFormatCaslFieldsWithTableNames<T extends keyof Tables> = {
|
||||||
|
// handle if any missing operator else throw error let the app break because this is executing again the db
|
||||||
|
missingOperatorCallback?: (operator: string) => void;
|
||||||
|
fieldMapping: (arg: string) => TFieldMapper<T> | null;
|
||||||
|
dynamicQuery: TKnexDynamicOperator;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatCaslOperatorFieldsWithTableNames = <T extends keyof Tables>({
|
||||||
|
missingOperatorCallback = (arg) => {
|
||||||
|
throw new BadRequestError({ message: `Unknown permission operator: ${arg}` });
|
||||||
|
},
|
||||||
|
dynamicQuery: dynamicQueryAst,
|
||||||
|
fieldMapping
|
||||||
|
}: TFormatCaslFieldsWithTableNames<T>) => {
|
||||||
|
const stack: [TKnexDynamicOperator, TKnexDynamicOperator | null][] = [[dynamicQueryAst, null]];
|
||||||
|
|
||||||
|
while (stack.length) {
|
||||||
|
const [filterAst, parentAst] = stack.pop()!;
|
||||||
|
|
||||||
|
if (filterAst.operator === "and" || filterAst.operator === "or" || filterAst.operator === "not") {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push([el, filterAst]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterAst.operator === "eq" ||
|
||||||
|
filterAst.operator === "ne" ||
|
||||||
|
filterAst.operator === "in" ||
|
||||||
|
filterAst.operator === "endsWith" ||
|
||||||
|
filterAst.operator === "startsWith"
|
||||||
|
) {
|
||||||
|
const attrPath = fieldMapping(filterAst.field);
|
||||||
|
if (attrPath) {
|
||||||
|
filterAst.field = attrPath;
|
||||||
|
} else if (parentAst && Array.isArray(parentAst.value)) {
|
||||||
|
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
|
||||||
|
} else throw new Error("Unknown casl field");
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentAst && Array.isArray(parentAst.value)) {
|
||||||
|
parentAst.value = parentAst.value.filter((childAst) => childAst !== filterAst) as string[];
|
||||||
|
} else {
|
||||||
|
missingOperatorCallback?.(filterAst.operator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dynamicQueryAst;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertCaslOperatorToKnexOperator = <T extends keyof Tables>(
|
||||||
|
caslKnexOperators: AbilityQuery,
|
||||||
|
fieldMapping: (arg: string) => TFieldMapper<T> | null
|
||||||
|
) => {
|
||||||
|
const value = [];
|
||||||
|
if (caslKnexOperators.$and) {
|
||||||
|
value.push({
|
||||||
|
operator: "not" as const,
|
||||||
|
value: caslKnexOperators.$and as TKnexDynamicOperator[]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (caslKnexOperators.$or) {
|
||||||
|
value.push({
|
||||||
|
operator: "or" as const,
|
||||||
|
value: caslKnexOperators.$or as TKnexDynamicOperator[]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatCaslOperatorFieldsWithTableNames({
|
||||||
|
dynamicQuery: {
|
||||||
|
operator: "and",
|
||||||
|
value
|
||||||
|
},
|
||||||
|
fieldMapping
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -81,25 +81,3 @@ export const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
|
|||||||
}
|
}
|
||||||
return chunks;
|
return chunks;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Returns all items from the first list that
|
|
||||||
* do not exist in the second list.
|
|
||||||
*/
|
|
||||||
export const diff = <T>(
|
|
||||||
root: readonly T[],
|
|
||||||
other: readonly T[],
|
|
||||||
identity: (item: T) => string | number | symbol = (t: T) => t as unknown as string | number | symbol
|
|
||||||
): T[] => {
|
|
||||||
if (!root?.length && !other?.length) return [];
|
|
||||||
if (root?.length === undefined) return [...other];
|
|
||||||
if (!other?.length) return [...root];
|
|
||||||
const bKeys = other.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
acc[identity(item)] = true;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string | number | symbol, boolean>
|
|
||||||
);
|
|
||||||
return root.filter((a) => !bKeys[identity(a)]);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2,31 +2,32 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { UnauthorizedError } from "../errors";
|
import { UnauthorizedError } from "../errors";
|
||||||
|
|
||||||
type TKnexDynamicPrimitiveOperator<T extends object> = {
|
type TKnexDynamicPrimitiveOperator = {
|
||||||
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
||||||
value: string;
|
value: string;
|
||||||
field: Extract<keyof T, string>;
|
field: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TKnexDynamicInOperator<T extends object> = {
|
type TKnexDynamicInOperator = {
|
||||||
operator: "in";
|
operator: "in";
|
||||||
value: string[] | number[];
|
value: string[] | number[];
|
||||||
field: Extract<keyof T, string>;
|
field: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TKnexNonGroupOperator<T extends object> = TKnexDynamicInOperator<T> | TKnexDynamicPrimitiveOperator<T>;
|
type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator;
|
||||||
|
|
||||||
type TKnexGroupOperator<T extends object> = {
|
type TKnexGroupOperator = {
|
||||||
operator: "and" | "or" | "not";
|
operator: "and" | "or" | "not";
|
||||||
value: (TKnexNonGroupOperator<T> | TKnexGroupOperator<T>)[];
|
value: (TKnexNonGroupOperator | TKnexGroupOperator)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TKnexDynamicOperator<T extends object> = TKnexGroupOperator<T> | TKnexNonGroupOperator<T>;
|
// akhilmhdh: This is still in pending state and not yet ready. If you want to use it ping me.
|
||||||
|
// used when you need to write a complex query with the orm
|
||||||
|
// use it when you need complex or and and condition - most of the time not needed
|
||||||
|
// majorly used with casl permission to filter data based on permission
|
||||||
|
export type TKnexDynamicOperator = TKnexGroupOperator | TKnexNonGroupOperator;
|
||||||
|
|
||||||
export const buildDynamicKnexQuery = <T extends object>(
|
export const buildDynamicKnexQuery = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => {
|
||||||
rootQueryBuild: Knex.QueryBuilder,
|
|
||||||
dynamicQuery: TKnexDynamicOperator<T>
|
|
||||||
) => {
|
|
||||||
const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }];
|
const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }];
|
||||||
|
|
||||||
while (stack.length) {
|
while (stack.length) {
|
||||||
@@ -49,25 +50,34 @@ export const buildDynamicKnexQuery = <T extends object>(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "and": {
|
case "and": {
|
||||||
filterAst.value.forEach((el) => {
|
void queryBuilder.andWhere((subQueryBuilder) => {
|
||||||
void queryBuilder.andWhere((subQueryBuilder) => {
|
filterAst.value.forEach((el) => {
|
||||||
buildDynamicKnexQuery(subQueryBuilder, el);
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "or": {
|
case "or": {
|
||||||
filterAst.value.forEach((el) => {
|
void queryBuilder.orWhere((subQueryBuilder) => {
|
||||||
void queryBuilder.orWhere((subQueryBuilder) => {
|
filterAst.value.forEach((el) => {
|
||||||
buildDynamicKnexQuery(subQueryBuilder, el);
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "not": {
|
case "not": {
|
||||||
filterAst.value.forEach((el) => {
|
void queryBuilder.whereNot((subQueryBuilder) => {
|
||||||
void queryBuilder.whereNot((subQueryBuilder) => {
|
filterAst.value.forEach((el) => {
|
||||||
buildDynamicKnexQuery(subQueryBuilder, el);
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Knex } from "knex";
|
|||||||
import { Tables } from "knex/types/tables";
|
import { Tables } from "knex/types/tables";
|
||||||
|
|
||||||
import { DatabaseError } from "../errors";
|
import { DatabaseError } from "../errors";
|
||||||
import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic";
|
|
||||||
|
|
||||||
export * from "./connection";
|
export * from "./connection";
|
||||||
export * from "./join";
|
export * from "./join";
|
||||||
@@ -21,10 +20,9 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
|||||||
export type TFindFilter<R extends object = object> = Partial<R> & {
|
export type TFindFilter<R extends object = object> = Partial<R> & {
|
||||||
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
||||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||||
$complex?: TKnexDynamicOperator<R>;
|
|
||||||
};
|
};
|
||||||
export const buildFindFilter =
|
export const buildFindFilter =
|
||||||
<R extends object = object>({ $in, $search, $complex, ...filter }: TFindFilter<R>) =>
|
<R extends object = object>({ $in, $search, ...filter }: TFindFilter<R>) =>
|
||||||
(bd: Knex.QueryBuilder<R, R>) => {
|
(bd: Knex.QueryBuilder<R, R>) => {
|
||||||
void bd.where(filter);
|
void bd.where(filter);
|
||||||
if ($in) {
|
if ($in) {
|
||||||
@@ -41,9 +39,6 @@ export const buildFindFilter =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ($complex) {
|
|
||||||
return buildDynamicKnexQuery(bd, $complex);
|
|
||||||
}
|
|
||||||
return bd;
|
return bd;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
void res.status(HttpStatusCodes.Forbidden).send({
|
void res.status(HttpStatusCodes.Forbidden).send({
|
||||||
statusCode: HttpStatusCodes.Forbidden,
|
statusCode: HttpStatusCodes.Forbidden,
|
||||||
error: "PermissionDenied",
|
error: "PermissionDenied",
|
||||||
message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}`
|
message: `You are not allowed to ${error.action} on ${error.subjectType}`
|
||||||
});
|
});
|
||||||
} else if (error instanceof ForbiddenRequestError) {
|
} else if (error instanceof ForbiddenRequestError) {
|
||||||
void res.status(HttpStatusCodes.Forbidden).send({
|
void res.status(HttpStatusCodes.Forbidden).send({
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
|
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
|
||||||
import { registerV1EERoutes } from "@app/ee/routes/v1";
|
import { registerV1EERoutes } from "@app/ee/routes/v1";
|
||||||
import { registerV2EERoutes } from "@app/ee/routes/v2";
|
|
||||||
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
|
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
|
||||||
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
|
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
|
||||||
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
|
||||||
@@ -1425,13 +1424,7 @@ export const registerRoutes = async (
|
|||||||
},
|
},
|
||||||
{ prefix: "/api/v1" }
|
{ prefix: "/api/v1" }
|
||||||
);
|
);
|
||||||
await server.register(
|
await server.register(registerV2Routes, { prefix: "/api/v2" });
|
||||||
async (v2Server) => {
|
|
||||||
await v2Server.register(registerV2EERoutes);
|
|
||||||
await v2Server.register(registerV2Routes);
|
|
||||||
},
|
|
||||||
{ prefix: "/api/v2" }
|
|
||||||
);
|
|
||||||
await server.register(registerV3Routes, { prefix: "/api/v3" });
|
await server.register(registerV3Routes, { prefix: "/api/v3" });
|
||||||
|
|
||||||
server.addHook("onClose", async () => {
|
server.addHook("onClose", async () => {
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ import {
|
|||||||
SecretApprovalPoliciesSchema,
|
SecretApprovalPoliciesSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
|
||||||
import { UnpackedPermissionSchema } from "./santizedSchemas/permission";
|
|
||||||
|
|
||||||
// sometimes the return data must be santizied to avoid leaking important values
|
// sometimes the return data must be santizied to avoid leaking important values
|
||||||
// always prefer pick over omit in zod
|
// always prefer pick over omit in zod
|
||||||
export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
|
export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const UnpackedPermissionSchema = z.object({
|
|
||||||
subject: z
|
|
||||||
.union([z.string().min(1), z.string().array()])
|
|
||||||
.transform((el) => (typeof el !== "string" ? el[0] : el))
|
|
||||||
.optional(),
|
|
||||||
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
|
|
||||||
conditions: z.unknown().optional(),
|
|
||||||
inverted: z.boolean().optional()
|
|
||||||
});
|
|
||||||
@@ -3,10 +3,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
|
import { SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas";
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
import { DASHBOARD } from "@app/lib/api-docs";
|
import { DASHBOARD } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
@@ -195,15 +192,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
req.permission.orgId
|
req.permission.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
const allowedDynamicSecretEnvironments = // filter envs user has access to
|
const permissiveEnvs = // filter envs user has access to
|
||||||
environments.filter((environment) =>
|
environments.filter((environment) =>
|
||||||
permission.can(
|
permission.can(
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) {
|
if (includeDynamicSecrets && permissiveEnvs.length) {
|
||||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -212,7 +209,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId,
|
projectId,
|
||||||
search,
|
search,
|
||||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
environmentSlugs: permissiveEnvs,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
isInternal: true
|
isInternal: true
|
||||||
});
|
});
|
||||||
@@ -227,7 +224,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
search,
|
search,
|
||||||
orderBy,
|
orderBy,
|
||||||
orderDirection,
|
orderDirection,
|
||||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
environmentSlugs: permissiveEnvs,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
limit: remainingLimit,
|
limit: remainingLimit,
|
||||||
offset: adjustedOffset,
|
offset: adjustedOffset,
|
||||||
@@ -244,13 +241,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeSecrets) {
|
if (includeSecrets && permissiveEnvs.length) {
|
||||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
environments,
|
environments: permissiveEnvs,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId,
|
projectId,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
@@ -263,7 +260,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
environments,
|
environments: permissiveEnvs,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId,
|
projectId,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
@@ -275,7 +272,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
isInternal: true
|
isInternal: true
|
||||||
});
|
});
|
||||||
|
|
||||||
for await (const environment of environments) {
|
for await (const environment of permissiveEnvs) {
|
||||||
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
|
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
|
||||||
|
|
||||||
if (secretCountFromEnv) {
|
if (secretCountFromEnv) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
|||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
import { fnSecretBulkInsert, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
|
import type { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
|
||||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||||
@@ -242,7 +242,7 @@ export const importDataIntoInfisicalFn = async ({
|
|||||||
}
|
}
|
||||||
await fnSecretBulkInsert({
|
await fnSecretBulkInsert({
|
||||||
inputSecrets: secretBatch.map((el) => {
|
inputSecrets: secretBatch.map((el) => {
|
||||||
const references = getAllSecretReferences(el.secretValue).nestedReferences;
|
const references = getAllNestedSecretReferences(el.secretValue);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
|
|||||||
@@ -67,8 +67,7 @@ const getIntegrationSecretsV2 = async (
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
secretImports,
|
allowedImports: secretImports
|
||||||
hasSecretAccess: () => true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||||
|
|||||||
@@ -89,10 +89,7 @@ export const integrationServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath })
|
||||||
environment: sourceEnvironment,
|
|
||||||
secretPath
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
|
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
|
||||||
@@ -165,10 +162,7 @@ export const integrationServiceFactory = ({
|
|||||||
if (environment || secretPath) {
|
if (environment || secretPath) {
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, { environment: newEnvironment, secretPath: newSecretPath })
|
||||||
environment: newEnvironment,
|
|
||||||
secretPath: newSecretPath
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
|||||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||||
|
|
||||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
|
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
|
||||||
|
|
||||||
import { ActorAuthMethod } from "../auth/auth-type";
|
import { ActorAuthMethod } from "../auth/auth-type";
|
||||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { RawRule } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
|
||||||
|
export const shouldCheckFolderPermission = (rules: RawRule[]) =>
|
||||||
|
rules.some((rule) => (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders));
|
||||||
@@ -12,6 +12,7 @@ import { OrderByDirection } from "@app/lib/types";
|
|||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||||
|
import { shouldCheckFolderPermission } from "./secret-folder-fns";
|
||||||
import {
|
import {
|
||||||
TCreateFolderDTO,
|
TCreateFolderDTO,
|
||||||
TDeleteFolderDTO,
|
TDeleteFolderDTO,
|
||||||
@@ -59,10 +60,20 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
ProjectPermissionActions.Create,
|
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||||
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
// for backwards compatibility, we handle authorization only when SecretFolders subject is used
|
||||||
);
|
if (shouldCheckFolderPermission(permission.rules)) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||||
if (!env) throw new NotFoundError({ message: "Environment not found", name: "Create folder" });
|
if (!env) throw new NotFoundError({ message: "Environment not found", name: "Create folder" });
|
||||||
@@ -150,10 +161,20 @@ export const secretFolderServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
folders.forEach(({ environment, path: secretPath }) => {
|
folders.forEach(({ environment, path: secretPath }) => {
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
ProjectPermissionActions.Edit,
|
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||||
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
// for backwards compatibility, we handle authorization only when SecretFolders subject is used
|
||||||
);
|
if (shouldCheckFolderPermission(permission.rules)) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await folderDAL.transaction(async (tx) =>
|
const result = await folderDAL.transaction(async (tx) =>
|
||||||
@@ -246,10 +267,20 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
ProjectPermissionActions.Edit,
|
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||||
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
// for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
|
||||||
);
|
if (shouldCheckFolderPermission(permission.rules)) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!parentFolder) throw new NotFoundError({ message: "Secret path not found" });
|
if (!parentFolder) throw new NotFoundError({ message: "Secret path not found" });
|
||||||
@@ -320,10 +351,20 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
ProjectPermissionActions.Delete,
|
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||||
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
// for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
|
||||||
);
|
if (shouldCheckFolderPermission(permission.rules)) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||||
if (!env) throw new NotFoundError({ message: "Environment not found", name: "Create folder" });
|
if (!env) throw new NotFoundError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ type TSecretImportSecretsV2 = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
id: string;
|
|
||||||
folderId: string | undefined;
|
folderId: string | undefined;
|
||||||
importFolderId: string;
|
importFolderId: string;
|
||||||
secrets: (TSecretsV2 & {
|
secrets: (TSecretsV2 & {
|
||||||
@@ -140,22 +139,24 @@ export const fnSecretsFromImports = async ({
|
|||||||
return secrets;
|
return secrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-disable no-await-in-loop, no-continue */
|
|
||||||
export const fnSecretsV2FromImports = async ({
|
export const fnSecretsV2FromImports = async ({
|
||||||
secretImports: rootSecretImports,
|
allowedImports: possibleCyclicImports,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
|
depth = 0,
|
||||||
|
cyclicDetector = new Set(),
|
||||||
decryptor,
|
decryptor,
|
||||||
expandSecretReferences,
|
expandSecretReferences
|
||||||
hasSecretAccess
|
|
||||||
}: {
|
}: {
|
||||||
secretImports: (Omit<TSecretImports, "importEnv"> & {
|
allowedImports: (Omit<TSecretImports, "importEnv"> & {
|
||||||
importEnv: { id: string; slug: string; name: string };
|
importEnv: { id: string; slug: string; name: string };
|
||||||
})[];
|
})[];
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
|
||||||
|
depth?: number;
|
||||||
|
cyclicDetector?: Set<string>;
|
||||||
decryptor: (value?: Buffer | null) => string;
|
decryptor: (value?: Buffer | null) => string;
|
||||||
expandSecretReferences?: (inputSecret: {
|
expandSecretReferences?: (inputSecret: {
|
||||||
value?: string;
|
value?: string;
|
||||||
@@ -163,107 +164,92 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
}) => Promise<string | undefined>;
|
}) => Promise<string | undefined>;
|
||||||
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const cyclicDetector = new Set();
|
// avoid going more than a depth
|
||||||
const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [
|
if (depth >= LEVEL_BREAK) return [];
|
||||||
{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }
|
|
||||||
];
|
|
||||||
|
|
||||||
const processedImports: TSecretImportSecretsV2[] = [];
|
const allowedImports = possibleCyclicImports.filter(
|
||||||
|
({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath))
|
||||||
|
);
|
||||||
|
|
||||||
while (stack.length) {
|
const importedFolders = (
|
||||||
const { secretImports, depth, parentImportedSecrets } = stack.pop()!;
|
await folderDAL.findByManySecretPath(
|
||||||
|
allowedImports.map(({ importEnv, importPath }) => ({
|
||||||
if (depth > LEVEL_BREAK) continue;
|
|
||||||
const sanitizedImports = secretImports.filter(
|
|
||||||
({ importPath, importEnv }) => !cyclicDetector.has(getImportUniqKey(importEnv.slug, importPath))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!sanitizedImports.length) continue;
|
|
||||||
|
|
||||||
const importedFolders = await folderDAL.findByManySecretPath(
|
|
||||||
sanitizedImports.map(({ importEnv, importPath }) => ({
|
|
||||||
envId: importEnv.id,
|
envId: importEnv.id,
|
||||||
secretPath: importPath
|
secretPath: importPath
|
||||||
}))
|
}))
|
||||||
);
|
)
|
||||||
if (!importedFolders.length) continue;
|
).filter(Boolean); // remove undefined ones
|
||||||
|
if (!importedFolders.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const importedFolderIds = importedFolders.map((el) => el?.id) as string[];
|
const importedFolderIds = importedFolders.map((el) => el?.id) as string[];
|
||||||
const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`);
|
const importedFolderGroupBySourceImport = groupBy(importedFolders, (i) => `${i?.envId}-${i?.path}`);
|
||||||
|
const importedSecrets = await secretDAL.find(
|
||||||
|
{
|
||||||
|
$in: { folderId: importedFolderIds },
|
||||||
|
type: SecretType.Shared
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sort: [["id", "asc"]]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const importedSecrets = await secretDAL.find(
|
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
|
||||||
{
|
|
||||||
$in: { folderId: importedFolderIds },
|
|
||||||
type: SecretType.Shared
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sort: [["id", "asc"]]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const importedSecretsGroupByFolderId = groupBy(importedSecrets, (i) => i.folderId);
|
|
||||||
|
|
||||||
sanitizedImports.forEach(({ importPath, importEnv }) => {
|
allowedImports.forEach(({ importPath, importEnv }) => {
|
||||||
cyclicDetector.add(getImportUniqKey(importEnv.slug, importPath));
|
cyclicDetector.add(getImportUniqKey(importEnv.slug, importPath));
|
||||||
});
|
});
|
||||||
// now we need to check recursively deeper imports made inside other imports
|
// now we need to check recursively deeper imports made inside other imports
|
||||||
// we go level wise meaning we take all imports of a tree level and then go deeper ones level by level
|
// we go level wise meaning we take all imports of a tree level and then go deeper ones level by level
|
||||||
const deeperImports = await secretImportDAL.findByFolderIds(importedFolderIds);
|
const deeperImports = await secretImportDAL.findByFolderIds(importedFolderIds);
|
||||||
const deeperImportsGroupByFolderId = groupBy(deeperImports, (i) => i.folderId);
|
let secretsFromDeeperImports: TSecretImportSecretsV2[] = [];
|
||||||
|
if (deeperImports.length) {
|
||||||
const isFirstIteration = !processedImports.length;
|
secretsFromDeeperImports = await fnSecretsV2FromImports({
|
||||||
sanitizedImports.forEach(({ importPath, importEnv, id, folderId }, i) => {
|
allowedImports: deeperImports.filter(({ isReplication }) => !isReplication),
|
||||||
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`]?.[0];
|
secretImportDAL,
|
||||||
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
|
folderDAL,
|
||||||
.filter((item) =>
|
secretDAL,
|
||||||
hasSecretAccess(
|
depth: depth + 1,
|
||||||
importEnv.slug,
|
cyclicDetector,
|
||||||
importPath,
|
decryptor,
|
||||||
item.key,
|
expandSecretReferences
|
||||||
item.tags.map((el) => el.slug)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.map((item) => ({
|
|
||||||
...item,
|
|
||||||
secretKey: item.key,
|
|
||||||
secretValue: decryptor(item.encryptedValue),
|
|
||||||
secretComment: decryptor(item.encryptedComment),
|
|
||||||
environment: importEnv.slug,
|
|
||||||
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
|
||||||
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (deeperImportsGroupByFolderId?.[sourceImportFolder?.id || ""]) {
|
|
||||||
stack.push({
|
|
||||||
secretImports: deeperImportsGroupByFolderId[sourceImportFolder?.id || ""],
|
|
||||||
depth: depth + 1,
|
|
||||||
parentImportedSecrets: secretsWithDuplicate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFirstIteration) {
|
|
||||||
processedImports.push({
|
|
||||||
secretPath: importPath,
|
|
||||||
environment: importEnv.slug,
|
|
||||||
environmentInfo: importEnv,
|
|
||||||
folderId: importedFolders?.[i]?.id,
|
|
||||||
id,
|
|
||||||
importFolderId: folderId,
|
|
||||||
secrets: secretsWithDuplicate
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
parentImportedSecrets.push(...secretsWithDuplicate);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/* eslint-enable */
|
const secretsFromdeeperImportGroupedByFolderId = groupBy(secretsFromDeeperImports, (i) => i.importFolderId);
|
||||||
|
|
||||||
|
const processedImports = allowedImports.map(({ importPath, importEnv, id, folderId }, i) => {
|
||||||
|
const sourceImportFolder = importedFolderGroupBySourceImport[`${importEnv.id}-${importPath}`]?.[0];
|
||||||
|
const folderDeeperImportSecrets =
|
||||||
|
secretsFromdeeperImportGroupedByFolderId?.[sourceImportFolder?.id || ""]?.[0]?.secrets || [];
|
||||||
|
const secretsWithDuplicate = (importedSecretsGroupByFolderId?.[importedFolders?.[i]?.id as string] || [])
|
||||||
|
.map((item) => ({
|
||||||
|
...item,
|
||||||
|
secretKey: item.key,
|
||||||
|
secretValue: decryptor(item.encryptedValue),
|
||||||
|
secretComment: decryptor(item.encryptedComment),
|
||||||
|
environment: importEnv.slug,
|
||||||
|
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||||
|
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||||
|
}))
|
||||||
|
.concat(folderDeeperImportSecrets);
|
||||||
|
|
||||||
|
return {
|
||||||
|
secretPath: importPath,
|
||||||
|
environment: importEnv.slug,
|
||||||
|
environmentInfo: importEnv,
|
||||||
|
folderId: importedFolders?.[i]?.id,
|
||||||
|
id,
|
||||||
|
importFolderId: folderId,
|
||||||
|
secrets: unique(secretsWithDuplicate, (el) => el.secretKey)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (expandSecretReferences) {
|
if (expandSecretReferences) {
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
processedImports.map((processedImport) => {
|
processedImports.map((processedImport) =>
|
||||||
// eslint-disable-next-line
|
Promise.allSettled(
|
||||||
processedImport.secrets = unique(processedImport.secrets, (i) => i.key);
|
|
||||||
return Promise.allSettled(
|
|
||||||
processedImport.secrets.map(async (decryptedSecret, index) => {
|
processedImport.secrets.map(async (decryptedSecret, index) => {
|
||||||
const expandedSecretValue = await expandSecretReferences({
|
const expandedSecretValue = await expandSecretReferences({
|
||||||
value: decryptedSecret.secretValue,
|
value: decryptedSecret.secretValue,
|
||||||
@@ -274,8 +260,8 @@ export const fnSecretsV2FromImports = async ({
|
|||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
processedImport.secrets[index].secretValue = expandedSecretValue || "";
|
processedImport.secrets[index].secretValue = expandedSecretValue || "";
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
})
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,12 +84,12 @@ export const secretImportServiceFactory = ({
|
|||||||
// check if user has permission to import into destination path
|
// check if user has permission to import into destination path
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Create,
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
// check if user has permission to import from target path
|
// check if user has permission to import from target path
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Create,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
environment: data.environment,
|
environment: data.environment,
|
||||||
secretPath: data.path
|
secretPath: data.path
|
||||||
@@ -191,7 +191,7 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
@@ -277,7 +277,7 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionActions.Delete,
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
@@ -342,8 +342,8 @@ export const secretImportServiceFactory = ({
|
|||||||
|
|
||||||
// check if user has permission to import into destination path
|
// check if user has permission to import into destination path
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Create,
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
@@ -366,7 +366,7 @@ export const secretImportServiceFactory = ({
|
|||||||
|
|
||||||
// check if user has permission to import from target path
|
// check if user has permission to import from target path
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Create,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
environment: secretImportDoc.importEnv.slug,
|
environment: secretImportDoc.importEnv.slug,
|
||||||
secretPath: secretImportDoc.importPath
|
secretPath: secretImportDoc.importPath
|
||||||
@@ -414,7 +414,7 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
@@ -446,7 +446,7 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
@@ -489,7 +489,7 @@ export const secretImportServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.SecretImports, {
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
environment: folder.environment.envSlug,
|
environment: folder.environment.envSlug,
|
||||||
secretPath: folderWithPath.path
|
secretPath: folderWithPath.path
|
||||||
})
|
})
|
||||||
@@ -532,19 +532,20 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) return [];
|
if (!folder) return [];
|
||||||
// this will already order by position
|
// this will already order by position
|
||||||
// so anything based on this order will also be in right position
|
// so anything based on this order will also be in right position
|
||||||
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
|
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
|
||||||
const allowedImports = secretImports.filter((el) =>
|
|
||||||
|
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
|
||||||
permission.can(
|
permission.can(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
environment: el.importEnv.slug,
|
environment: importEnv.slug,
|
||||||
secretPath: el.importPath
|
secretPath: importPath
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -569,7 +570,7 @@ export const secretImportServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
);
|
);
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) return [];
|
if (!folder) return [];
|
||||||
@@ -577,6 +578,16 @@ export const secretImportServiceFactory = ({
|
|||||||
// so anything based on this order will also be in right position
|
// so anything based on this order will also be in right position
|
||||||
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
|
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
|
||||||
|
|
||||||
|
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
|
||||||
|
permission.can(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: importEnv.slug,
|
||||||
|
secretPath: importPath
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
if (shouldUseSecretV2Bridge) {
|
if (shouldUseSecretV2Bridge) {
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
@@ -584,21 +595,11 @@ export const secretImportServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
const importedSecrets = await fnSecretsV2FromImports({
|
const importedSecrets = await fnSecretsV2FromImports({
|
||||||
secretImports,
|
allowedImports,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||||
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
|
|
||||||
permission.can(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: expandEnvironment,
|
|
||||||
secretPath: expandSecretPath,
|
|
||||||
secretName: expandSecretKey,
|
|
||||||
secretTags: expandSecretTags
|
|
||||||
})
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
return importedSecrets;
|
return importedSecrets;
|
||||||
}
|
}
|
||||||
@@ -609,21 +610,7 @@ export const secretImportServiceFactory = ({
|
|||||||
name: "bot_not_found_error"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
|
|
||||||
const allowedImports = secretImports.filter((el) =>
|
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
|
||||||
permission.can(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: el.importEnv.slug,
|
|
||||||
secretPath: el.importPath
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const importedSecrets = await fnSecretsFromImports({
|
|
||||||
allowedImports,
|
|
||||||
folderDAL,
|
|
||||||
secretDAL,
|
|
||||||
secretImportDAL
|
|
||||||
});
|
|
||||||
return importedSecrets.map((el) => ({
|
return importedSecrets.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
secrets: el.secrets.map((encryptedSecret) =>
|
secrets: el.secrets.map((encryptedSecret) =>
|
||||||
|
|||||||
@@ -4,14 +4,7 @@ import { validate as uuidValidate } from "uuid";
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
|
import { SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecretsV2Update } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import {
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
buildFindFilter,
|
|
||||||
ormify,
|
|
||||||
selectAllTableCols,
|
|
||||||
sqlNestRelationships,
|
|
||||||
TFindFilter,
|
|
||||||
TFindOpt
|
|
||||||
} from "@app/lib/knex";
|
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
@@ -20,97 +13,6 @@ export type TSecretV2BridgeDALFactory = ReturnType<typeof secretV2BridgeDALFacto
|
|||||||
export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||||
const secretOrm = ormify(db, TableName.SecretV2);
|
const secretOrm = ormify(db, TableName.SecretV2);
|
||||||
|
|
||||||
const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
|
|
||||||
try {
|
|
||||||
const docs = await (tx || db)(TableName.SecretV2)
|
|
||||||
.where(filter)
|
|
||||||
.leftJoin(
|
|
||||||
TableName.SecretV2JnTag,
|
|
||||||
`${TableName.SecretV2}.id`,
|
|
||||||
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
TableName.SecretTag,
|
|
||||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
|
||||||
`${TableName.SecretTag}.id`
|
|
||||||
)
|
|
||||||
.select(selectAllTableCols(TableName.SecretV2))
|
|
||||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
|
||||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
|
||||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
|
||||||
|
|
||||||
const data = sqlNestRelationships({
|
|
||||||
data: docs,
|
|
||||||
key: "id",
|
|
||||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
|
||||||
childrenMapper: [
|
|
||||||
{
|
|
||||||
key: "tagId",
|
|
||||||
label: "tags" as const,
|
|
||||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
|
||||||
id,
|
|
||||||
color,
|
|
||||||
slug,
|
|
||||||
name: slug
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
return data?.[0];
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: `${TableName.SecretV2}: FindOne` });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const find = async (filter: TFindFilter<TSecretsV2>, { offset, limit, sort, tx }: TFindOpt<TSecretsV2> = {}) => {
|
|
||||||
try {
|
|
||||||
const query = (tx || db)(TableName.SecretV2)
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
.where(buildFindFilter(filter))
|
|
||||||
.leftJoin(
|
|
||||||
TableName.SecretV2JnTag,
|
|
||||||
`${TableName.SecretV2}.id`,
|
|
||||||
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
TableName.SecretTag,
|
|
||||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
|
||||||
`${TableName.SecretTag}.id`
|
|
||||||
)
|
|
||||||
.select(selectAllTableCols(TableName.SecretV2))
|
|
||||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
|
||||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
|
||||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
|
||||||
if (limit) void query.limit(limit);
|
|
||||||
if (offset) void query.offset(offset);
|
|
||||||
if (sort) {
|
|
||||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
|
||||||
}
|
|
||||||
|
|
||||||
const docs = await query;
|
|
||||||
const data = sqlNestRelationships({
|
|
||||||
data: docs,
|
|
||||||
key: "id",
|
|
||||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
|
||||||
childrenMapper: [
|
|
||||||
{
|
|
||||||
key: "tagId",
|
|
||||||
label: "tags" as const,
|
|
||||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
|
||||||
id,
|
|
||||||
color,
|
|
||||||
slug,
|
|
||||||
name: slug
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: `${TableName.SecretV2}: Find` });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const update = async (filter: Partial<TSecretsV2>, data: Omit<TSecretsV2Update, "version">, tx?: Knex) => {
|
const update = async (filter: Partial<TSecretsV2>, data: Omit<TSecretsV2Update, "version">, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const sec = await (tx || db)(TableName.SecretV2)
|
const sec = await (tx || db)(TableName.SecretV2)
|
||||||
@@ -582,8 +484,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
upsertSecretReferences,
|
upsertSecretReferences,
|
||||||
findReferencedSecretReferences,
|
findReferencedSecretReferences,
|
||||||
findAllProjectSecretValues,
|
findAllProjectSecretValues,
|
||||||
countByFolderIds,
|
countByFolderIds
|
||||||
findOne,
|
|
||||||
find
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ export const shouldUseSecretV2Bridge = (version: number) => version === 3;
|
|||||||
* // { environment: 'prod', secretPath: '/anotherFolder' }
|
* // { environment: 'prod', secretPath: '/anotherFolder' }
|
||||||
* // ]
|
* // ]
|
||||||
*/
|
*/
|
||||||
export const getAllSecretReferences = (maybeSecretReference: string) => {
|
export const getAllNestedSecretReferences = (maybeSecretReference: string) => {
|
||||||
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
|
const references = Array.from(maybeSecretReference.matchAll(INTERPOLATION_SYNTAX_REG), (m) => m[1]);
|
||||||
|
return references
|
||||||
const nestedReferences = references
|
|
||||||
.filter((el) => el.includes("."))
|
.filter((el) => el.includes("."))
|
||||||
.map((el) => {
|
.map((el) => {
|
||||||
const [environment, ...secretPathList] = el.split(".");
|
const [environment, ...secretPathList] = el.split(".");
|
||||||
@@ -43,8 +42,6 @@ export const getAllSecretReferences = (maybeSecretReference: string) => {
|
|||||||
secretKey: secretPathList[secretPathList.length - 1]
|
secretKey: secretPathList[secretPathList.length - 1]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const localReferences = references.filter((el) => !el.includes("."));
|
|
||||||
return { nestedReferences, localReferences };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// these functions are special functions shared by a couple of resources
|
// these functions are special functions shared by a couple of resources
|
||||||
@@ -328,13 +325,16 @@ type TRecursivelyFetchSecretsFromFoldersArg = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
|
hasAccess: (environment: string, secretPath: string) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const recursivelyGetSecretPaths = async ({
|
export const recursivelyGetSecretPaths = async ({
|
||||||
folderDAL,
|
folderDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectId,
|
projectId,
|
||||||
environment
|
environment,
|
||||||
|
currentPath,
|
||||||
|
hasAccess
|
||||||
}: TRecursivelyFetchSecretsFromFoldersArg) => {
|
}: TRecursivelyFetchSecretsFromFoldersArg) => {
|
||||||
const env = await projectEnvDAL.findOne({
|
const env = await projectEnvDAL.findOne({
|
||||||
projectId,
|
projectId,
|
||||||
@@ -360,7 +360,12 @@ export const recursivelyGetSecretPaths = async ({
|
|||||||
folderId: p.folderId
|
folderId: p.folderId
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return paths;
|
// Filter out paths that the user does not have permission to access, and paths that are not in the current path
|
||||||
|
const allowedPaths = paths.filter(
|
||||||
|
(folder) => hasAccess(environment, folder.path) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
return allowedPaths;
|
||||||
};
|
};
|
||||||
// used to convert multi line ones to quotes ones with \n
|
// used to convert multi line ones to quotes ones with \n
|
||||||
const formatMultiValueEnv = (val?: string) => {
|
const formatMultiValueEnv = (val?: string) => {
|
||||||
@@ -374,7 +379,7 @@ type TInterpolateSecretArg = {
|
|||||||
decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined;
|
decryptSecretValue: (encryptedValue?: Buffer | null) => string | undefined;
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderId">;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||||
canExpandValue: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
|
canExpandValue: (environment: string, secretPath: string) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_SECRET_REFERENCE_DEPTH = 10;
|
const MAX_SECRET_REFERENCE_DEPTH = 10;
|
||||||
@@ -385,29 +390,29 @@ export const expandSecretReferencesFactory = ({
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
canExpandValue
|
canExpandValue
|
||||||
}: TInterpolateSecretArg) => {
|
}: TInterpolateSecretArg) => {
|
||||||
const secretCache: Record<string, Record<string, { value: string; tags: string[] }>> = {};
|
const secretCache: Record<string, Record<string, string>> = {};
|
||||||
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
|
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
|
||||||
|
|
||||||
const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => {
|
const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => {
|
||||||
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
||||||
|
|
||||||
if (secretCache?.[cacheKey]) {
|
if (secretCache?.[cacheKey]) {
|
||||||
return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
|
return secretCache[cacheKey][secretKey] || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder) return { value: "", tags: [] };
|
if (!folder) return "";
|
||||||
const secrets = await secretDAL.findByFolderId(folder.id);
|
const secrets = await secretDAL.findByFolderId(folder.id);
|
||||||
|
|
||||||
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
|
const decryptedSecret = secrets.reduce<Record<string, string>>((prev, secret) => {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
prev[secret.key] = { value: decryptSecret(secret.encryptedValue) || "", tags: secret.tags?.map((el) => el.slug) };
|
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
|
||||||
return prev;
|
return prev;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
secretCache[cacheKey] = decryptedSecret;
|
secretCache[cacheKey] = decryptedSecret;
|
||||||
|
|
||||||
return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
|
return secretCache[cacheKey][secretKey] || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => {
|
const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => {
|
||||||
@@ -433,43 +438,43 @@ export const expandSecretReferencesFactory = ({
|
|||||||
if (entities.length === 1) {
|
if (entities.length === 1) {
|
||||||
const [secretKey] = entities;
|
const [secretKey] = entities;
|
||||||
|
|
||||||
// eslint-disable-next-line no-continue,no-await-in-loop
|
if (!canExpandValue(environment, secretPath))
|
||||||
const referredValue = await fetchSecret(environment, secretPath, secretKey);
|
|
||||||
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
|
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.`
|
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue,no-await-in-loop
|
||||||
|
const referedValue = await fetchSecret(environment, secretPath, secretKey);
|
||||||
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
||||||
secretCache[cacheKey][secretKey] = referredValue;
|
secretCache[cacheKey][secretKey] = referedValue;
|
||||||
if (INTERPOLATION_SYNTAX_REG.test(referredValue.value)) {
|
if (INTERPOLATION_SYNTAX_REG.test(referedValue)) {
|
||||||
stack.push({
|
stack.push({
|
||||||
value: referredValue.value,
|
value: referedValue,
|
||||||
secretPath,
|
secretPath,
|
||||||
environment,
|
environment,
|
||||||
depth: depth + 1
|
depth: depth + 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (referredValue) {
|
if (referedValue) {
|
||||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referredValue.value);
|
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const secretReferenceEnvironment = entities[0];
|
const secretReferenceEnvironment = entities[0];
|
||||||
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
|
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
|
||||||
const secretReferenceKey = entities[entities.length - 1];
|
const secretReferenceKey = entities[entities.length - 1];
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath))
|
||||||
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
|
|
||||||
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
|
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.`
|
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
|
||||||
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
|
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
|
||||||
secretCache[cacheKey][secretReferenceKey] = referedValue;
|
secretCache[cacheKey][secretReferenceKey] = referedValue;
|
||||||
if (INTERPOLATION_SYNTAX_REG.test(referedValue.value)) {
|
if (INTERPOLATION_SYNTAX_REG.test(referedValue)) {
|
||||||
stack.push({
|
stack.push({
|
||||||
value: referedValue.value,
|
value: referedValue,
|
||||||
secretPath: secretReferencePath,
|
secretPath: secretReferencePath,
|
||||||
environment: secretReferenceEnvironment,
|
environment: secretReferenceEnvironment,
|
||||||
depth: depth + 1
|
depth: depth + 1
|
||||||
@@ -477,7 +482,7 @@ export const expandSecretReferencesFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (referedValue) {
|
if (referedValue) {
|
||||||
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue.value);
|
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,6 @@ type TPartialSecret = Pick<TSecretsV2, "id" | "reminderRepeatDays" | "reminderNo
|
|||||||
|
|
||||||
type TPartialInputSecret = Pick<TSecretsV2, "type" | "reminderNote" | "reminderRepeatDays" | "id">;
|
type TPartialInputSecret = Pick<TSecretsV2, "type" | "reminderNote" | "reminderRepeatDays" | "id">;
|
||||||
|
|
||||||
export type TSecretReferenceDTO = {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secretKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TGetSecretsDTO = {
|
export type TGetSecretsDTO = {
|
||||||
expandSecretReferences?: boolean;
|
expandSecretReferences?: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import {
|
import {
|
||||||
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
||||||
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
||||||
getAllSecretReferences
|
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
|
||||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
@@ -791,7 +791,7 @@ export const createManySecretsRawFnFactory = ({
|
|||||||
: null,
|
: null,
|
||||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||||
tags: secret.tags,
|
tags: secret.tags,
|
||||||
references: getAllSecretReferences(secret.secretValue).nestedReferences
|
references: getAllNestedSecretReferencesV2Bridge(secret.secretValue)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -971,7 +971,7 @@ export const updateManySecretsRawFnFactory = ({
|
|||||||
: null,
|
: null,
|
||||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||||
tags: secret.tags,
|
tags: secret.tags,
|
||||||
references: getAllSecretReferences(secret.secretValue).nestedReferences
|
references: getAllNestedSecretReferencesV2Bridge(secret.secretValue)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
|||||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { expandSecretReferencesFactory, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
import { expandSecretReferencesFactory, getAllNestedSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
|
||||||
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
@@ -342,8 +342,7 @@ export const secretQueueFactory = ({
|
|||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
expandSecretReferences,
|
expandSecretReferences,
|
||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
secretImports,
|
allowedImports: secretImports
|
||||||
hasSecretAccess: () => true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||||
@@ -1148,7 +1147,7 @@ export const secretQueueFactory = ({
|
|||||||
: "";
|
: "";
|
||||||
const encryptedValue = secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob;
|
const encryptedValue = secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob;
|
||||||
// create references
|
// create references
|
||||||
const references = getAllSecretReferences(value).nestedReferences;
|
const references = getAllNestedSecretReferences(value);
|
||||||
secretReferences.push({ secretId: el.id, references });
|
secretReferences.push({ secretId: el.id, references });
|
||||||
|
|
||||||
const encryptedComment = comment
|
const encryptedComment = comment
|
||||||
|
|||||||
@@ -2407,26 +2407,17 @@ export const secretServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionActions.Delete,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath: sourceSecretPath })
|
||||||
environment: sourceEnvironment,
|
|
||||||
secretPath: sourceSecretPath
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Create,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath })
|
||||||
environment: destinationEnvironment,
|
|
||||||
secretPath: destinationSecretPath
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, { environment: destinationEnvironment, secretPath: destinationSecretPath })
|
||||||
environment: destinationEnvironment,
|
|
||||||
secretPath: destinationSecretPath
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { botKey } = await projectBotService.getBotKey(project.id);
|
const { botKey } = await projectBotService.getBotKey(project.id);
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import picomatch from "picomatch";
|
|
||||||
|
|
||||||
import { FormControl } from "../v2/FormControl";
|
|
||||||
import { Input } from "../v2/Input";
|
|
||||||
|
|
||||||
export const GlobPermissionInfo = () => {
|
|
||||||
const [pattern, setPattern] = useState("");
|
|
||||||
const [text, setText] = useState("");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mt-2">A glob pattern uses wildcards to match resources or paths.</div>
|
|
||||||
<div>
|
|
||||||
<FormControl label="Glob pattern" helperText="Examples: /{a,b}, DB_**">
|
|
||||||
<Input value={pattern} onChange={(e) => setPattern(e.target.value)} />
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormControl
|
|
||||||
label="Test string"
|
|
||||||
helperText="Type a value to test glob match"
|
|
||||||
isError={
|
|
||||||
pattern && text ? !picomatch.isMatch(text, pattern, { strictSlashes: false }) : false
|
|
||||||
}
|
|
||||||
errorText="Invalid"
|
|
||||||
>
|
|
||||||
<Input value={text} onChange={(e) => setText(e.target.value)} />
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,25 +1,23 @@
|
|||||||
import { FunctionComponent, ReactNode } from "react";
|
import { FunctionComponent, ReactNode } from "react";
|
||||||
import { AbilityTuple, MongoAbility } from "@casl/ability";
|
import { BoundCanProps, Can } from "@casl/react";
|
||||||
import { Can } from "@casl/react";
|
|
||||||
|
|
||||||
import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext";
|
import { TProjectPermission, useProjectPermission } from "@app/context/ProjectPermissionContext";
|
||||||
|
|
||||||
import { Tooltip } from "../v2/Tooltip";
|
import { Tooltip } from "../v2";
|
||||||
|
|
||||||
type Props<T extends AbilityTuple> = {
|
type Props = {
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
// this prop is used when there exist already a tooltip as helper text for users
|
// this prop is used when there exist already a tooltip as helper text for users
|
||||||
// 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;
|
||||||
children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode);
|
// BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission
|
||||||
passThrough?: boolean;
|
// For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error
|
||||||
I: T[0];
|
// tried a lot combinations
|
||||||
a: T[1];
|
// REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15
|
||||||
ability?: MongoAbility<T>;
|
} & BoundCanProps<any>;
|
||||||
};
|
|
||||||
|
|
||||||
export const ProjectPermissionCan: FunctionComponent<Props<ProjectPermissionSet>> = ({
|
export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
||||||
label = "Access restricted",
|
label = "Access restricted",
|
||||||
children,
|
children,
|
||||||
passThrough = true,
|
passThrough = true,
|
||||||
@@ -33,7 +31,9 @@ export const ProjectPermissionCan: FunctionComponent<Props<ProjectPermissionSet>
|
|||||||
{(isAllowed, ability) => {
|
{(isAllowed, ability) => {
|
||||||
// akhilmhdh: This is set as type due to error in casl react type.
|
// akhilmhdh: This is set as type due to error in casl react type.
|
||||||
const finalChild =
|
const finalChild =
|
||||||
typeof children === "function" ? children(isAllowed, ability as any) : children;
|
typeof children === "function"
|
||||||
|
? children(isAllowed, ability as TProjectPermission)
|
||||||
|
: children;
|
||||||
|
|
||||||
if (!isAllowed && passThrough) {
|
if (!isAllowed && passThrough) {
|
||||||
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { GlobPermissionInfo } from "./GlobPermissionInfo";
|
|
||||||
export { OrgPermissionCan } from "./OrgPermissionCan";
|
export { OrgPermissionCan } from "./OrgPermissionCan";
|
||||||
export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
|
export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
|
||||||
export { ProjectPermissionCan } from "./ProjectPermissionCan";
|
export { ProjectPermissionCan } from "./ProjectPermissionCan";
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ type Props = {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
dropdownContainerClassName?: string;
|
dropdownContainerClassName?: string;
|
||||||
containerClassName?: string;
|
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
position?: "item-aligned" | "popper";
|
position?: "item-aligned" | "popper";
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
@@ -32,13 +31,12 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
dropdownContainerClassName,
|
dropdownContainerClassName,
|
||||||
position,
|
position,
|
||||||
containerClassName,
|
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className={twMerge("flex items-center space-x-2", containerClassName)}>
|
<div className="flex items-center space-x-2">
|
||||||
<SelectPrimitive.Root
|
<SelectPrimitive.Root
|
||||||
{...props}
|
{...props}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
|||||||
export {
|
export {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionDynamicSecretActions {
|
|
||||||
ReadRootCredential = "read-root-credential",
|
|
||||||
CreateRootCredential = "create-root-credential",
|
|
||||||
EditRootCredential = "edit-root-credential",
|
|
||||||
DeleteRootCredential = "delete-root-credential",
|
|
||||||
Lease = "lease"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ProjectPermissionCmekActions {
|
export enum ProjectPermissionCmekActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@@ -29,7 +21,7 @@ export enum PermissionConditionOperators {
|
|||||||
$ALL = "$all",
|
$ALL = "$all",
|
||||||
$REGEX = "$regex",
|
$REGEX = "$regex",
|
||||||
$EQ = "$eq",
|
$EQ = "$eq",
|
||||||
$NEQ = "$ne",
|
$NEQ = "$neq",
|
||||||
$GLOB = "$glob"
|
$GLOB = "$glob"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +37,7 @@ export type TPermissionConditionOperators = {
|
|||||||
export type TPermissionCondition = Record<
|
export type TPermissionCondition = Record<
|
||||||
string,
|
string,
|
||||||
| string
|
| string
|
||||||
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $ne: string; $glob: string }
|
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $neq: string; $glob: string }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
@@ -60,11 +52,9 @@ export enum ProjectPermissionSub {
|
|||||||
Tags = "tags",
|
Tags = "tags",
|
||||||
AuditLogs = "audit-logs",
|
AuditLogs = "audit-logs",
|
||||||
IpAllowList = "ip-allowlist",
|
IpAllowList = "ip-allowlist",
|
||||||
Project = "workspace",
|
Workspace = "workspace",
|
||||||
Secrets = "secrets",
|
Secrets = "secrets",
|
||||||
SecretFolders = "secret-folders",
|
SecretFolders = "secret-folders",
|
||||||
SecretImports = "secret-imports",
|
|
||||||
DynamicSecrets = "dynamic-secrets",
|
|
||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
@@ -78,24 +68,7 @@ export enum ProjectPermissionSub {
|
|||||||
Cmek = "cmek"
|
Cmek = "cmek"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SecretSubjectFields = {
|
type SubjectFields = {
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secretName: string;
|
|
||||||
secretTags: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SecretFolderSubjectFields = {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DynamicSecretSubjectFields = {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SecretImportSubjectFields = {
|
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
@@ -103,30 +76,13 @@ export type SecretImportSubjectFields = {
|
|||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
(
|
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||||
| ProjectPermissionSub.Secrets
|
|
||||||
| (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
(
|
(
|
||||||
| ProjectPermissionSub.SecretFolders
|
| ProjectPermissionSub.SecretFolders
|
||||||
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
|
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
||||||
)
|
|
||||||
]
|
|
||||||
| [
|
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
(
|
|
||||||
| ProjectPermissionSub.DynamicSecrets
|
|
||||||
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
| [
|
|
||||||
ProjectPermissionActions,
|
|
||||||
(
|
|
||||||
| ProjectPermissionSub.SecretImports
|
|
||||||
| (ForcedSubject<ProjectPermissionSub.SecretImports> & SecretImportSubjectFields)
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||||
@@ -139,19 +95,19 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
|
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek];
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
|
||||||
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
export type TProjectPermission = MongoAbility<ProjectPermissionSet>;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export type { TProjectPermission } from "./ProjectPermissionContext";
|
|||||||
export {
|
export {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionProvider,
|
ProjectPermissionProvider,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission
|
useProjectPermission
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
import { ComponentType } from "react";
|
import { ComponentType } from "react";
|
||||||
import { AbilityTuple } from "@casl/ability";
|
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability";
|
||||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { useProjectPermission } from "@app/context";
|
import { TProjectPermission, useProjectPermission } from "@app/context";
|
||||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
|
||||||
|
|
||||||
type Props<T extends AbilityTuple> = {
|
type Props<T extends Abilities> = (T extends AbilityTuple
|
||||||
className?: string;
|
? {
|
||||||
containerClassName?: string;
|
action: T[0];
|
||||||
action: T[0];
|
subject: Extract<T[1], SubjectType>;
|
||||||
subject: T[1];
|
}
|
||||||
};
|
: {
|
||||||
|
action: string;
|
||||||
|
subject: string;
|
||||||
|
}) & { className?: string; containerClassName?: string };
|
||||||
|
|
||||||
export const withProjectPermission = <T extends {}>(
|
export const withProjectPermission = <T extends {}, J extends TProjectPermission>(
|
||||||
Component: ComponentType<Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T>,
|
Component: ComponentType<T>,
|
||||||
{ action, subject, className, containerClassName }: Props<ProjectPermissionSet>
|
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
|
||||||
) => {
|
) => {
|
||||||
const HOC = (hocProps: Omit<Props<ProjectPermissionSet>, "action" | "subject"> & T) => {
|
const HOC = (hocProps: T) => {
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
// akhilmhdh: Set as any due to casl/react ts type bug
|
// akhilmhdh: Set as any due to casl/react ts type bug
|
||||||
// REASON: casl due to its type checking can't seem to union even if union intersection is applied
|
// REASON: casl due to its type checking can't seem to union even if union intersection is applied
|
||||||
if (permission.cannot(action as any, subject as any)) {
|
if (permission.cannot(action as any, subject)) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
} from "@app/hooks/api/dashboard/types";
|
} from "@app/hooks/api/dashboard/types";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
|
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
|
||||||
import { unique } from "@app/lib/fn/array";
|
|
||||||
|
|
||||||
export const dashboardKeys = {
|
export const dashboardKeys = {
|
||||||
all: () => ["dashboard"] as const,
|
all: () => ["dashboard"] as const,
|
||||||
@@ -155,20 +154,10 @@ export const useGetProjectSecretsOverview = (
|
|||||||
},
|
},
|
||||||
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => {
|
select: useCallback((data: Awaited<ReturnType<typeof fetchProjectSecretsOverview>>) => {
|
||||||
const { secrets, ...select } = data;
|
const { secrets, ...select } = data;
|
||||||
const uniqueSecrets = secrets ? unique(secrets, (i) => i.secretKey) : [];
|
|
||||||
|
|
||||||
const uniqueFolders = select.folders ? unique(select.folders, (i) => i.name) : [];
|
|
||||||
|
|
||||||
const uniqueDynamicSecrets = select.dynamicSecrets
|
|
||||||
? unique(select.dynamicSecrets, (i) => i.name)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...select,
|
...select,
|
||||||
secrets: secrets ? mergePersonalSecrets(secrets) : undefined,
|
secrets: secrets ? mergePersonalSecrets(secrets) : undefined
|
||||||
totalUniqueSecretsInPage: uniqueSecrets.length,
|
|
||||||
totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length,
|
|
||||||
totalUniqueFoldersInPage: uniqueFolders.length
|
|
||||||
};
|
};
|
||||||
}, []),
|
}, []),
|
||||||
keepPreviousData: true
|
keepPreviousData: true
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ export type DashboardProjectSecretsOverviewResponse = {
|
|||||||
totalFolderCount?: number;
|
totalFolderCount?: number;
|
||||||
totalDynamicSecretCount?: number;
|
totalDynamicSecretCount?: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
totalUniqueSecretsInPage: number;
|
|
||||||
totalUniqueDynamicSecretsInPage: number;
|
|
||||||
totalUniqueFoldersInPage: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardProjectSecretsDetailsResponse = {
|
export type DashboardProjectSecretsDetailsResponse = {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { packRules } from "@casl/ability/extra";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
@@ -15,7 +16,10 @@ export const useCreateProjectUserAdditionalPrivilege = () => {
|
|||||||
|
|
||||||
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({
|
return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({
|
||||||
mutationFn: async (dto) => {
|
mutationFn: async (dto) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", dto);
|
const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", {
|
||||||
|
...dto,
|
||||||
|
permissions: packRules(dto.permissions)
|
||||||
|
});
|
||||||
return data.privilege;
|
return data.privilege;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { projectMembershipId }) => {
|
onSuccess: (_, { projectMembershipId }) => {
|
||||||
@@ -31,7 +35,7 @@ export const useUpdateProjectUserAdditionalPrivilege = () => {
|
|||||||
mutationFn: async (dto) => {
|
mutationFn: async (dto) => {
|
||||||
const { data } = await apiRequest.patch(
|
const { data } = await apiRequest.patch(
|
||||||
`/api/v1/additional-privilege/users/${dto.privilegeId}`,
|
`/api/v1/additional-privilege/users/${dto.privilegeId}`,
|
||||||
dto
|
{ ...dto, permissions: dto.permissions ? packRules(dto.permissions) : undefined }
|
||||||
);
|
);
|
||||||
return data.privilege;
|
return data.privilege;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
@@ -17,7 +18,10 @@ const fetchProjectUserPrivilegeDetails = async (privilegeId: string) => {
|
|||||||
} = await apiRequest.get<{
|
} = await apiRequest.get<{
|
||||||
privilege: Omit<TProjectUserPrivilege, "permissions"> & { permissions: unknown };
|
privilege: Omit<TProjectUserPrivilege, "permissions"> & { permissions: unknown };
|
||||||
}>(`/api/v1/additional-privilege/users/${privilegeId}`);
|
}>(`/api/v1/additional-privilege/users/${privilegeId}`);
|
||||||
return privilege;
|
return {
|
||||||
|
...privilege,
|
||||||
|
permissions: unpackRules(privilege.permissions as PackRule<TProjectPermission>[])
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetProjectUserPrivilegeDetails = (privilegeId: string) => {
|
export const useGetProjectUserPrivilegeDetails = (privilegeId: string) => {
|
||||||
@@ -40,7 +44,7 @@ export const useListProjectUserPrivileges = (projectMembershipId: string) => {
|
|||||||
}>("/api/v1/additional-privilege/users", { params: { projectMembershipId } });
|
}>("/api/v1/additional-privilege/users", { params: { projectMembershipId } });
|
||||||
return privileges.map((el) => ({
|
return privileges.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
permissions: el.permissions as TProjectPermission[]
|
permissions: unpackRules(el.permissions as PackRule<TProjectPermission>[])
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,15 +4,6 @@ export enum ProjectUserAdditionalPrivilegeTemporaryMode {
|
|||||||
Relative = "relative"
|
Relative = "relative"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TProjectSpecificPrivilegePermission = {
|
|
||||||
conditions: {
|
|
||||||
environment: string;
|
|
||||||
secretPath?: { $glob: string };
|
|
||||||
};
|
|
||||||
actions: string[];
|
|
||||||
subject: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TProjectUserPrivilege = {
|
export type TProjectUserPrivilege = {
|
||||||
projectMembershipId: string;
|
projectMembershipId: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -21,21 +12,21 @@ export type TProjectUserPrivilege = {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
permissions?: TProjectPermission[];
|
permissions?: TProjectPermission[];
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
isTemporary: true;
|
isTemporary: true;
|
||||||
temporaryMode: string;
|
temporaryMode: string;
|
||||||
temporaryRange: string;
|
temporaryRange: string;
|
||||||
temporaryAccessStartTime: string;
|
temporaryAccessStartTime: string;
|
||||||
temporaryAccessEndTime?: string;
|
temporaryAccessEndTime?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
isTemporary: false;
|
isTemporary: false;
|
||||||
temporaryMode?: null;
|
temporaryMode?: null;
|
||||||
temporaryRange?: null;
|
temporaryRange?: null;
|
||||||
temporaryAccessStartTime?: null;
|
temporaryAccessStartTime?: null;
|
||||||
temporaryAccessEndTime?: null;
|
temporaryAccessEndTime?: null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TCreateProjectUserPrivilegeDTO = {
|
export type TCreateProjectUserPrivilegeDTO = {
|
||||||
projectMembershipId: string;
|
projectMembershipId: string;
|
||||||
@@ -44,7 +35,7 @@ export type TCreateProjectUserPrivilegeDTO = {
|
|||||||
temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode;
|
temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode;
|
||||||
temporaryRange?: string;
|
temporaryRange?: string;
|
||||||
temporaryAccessStartTime?: string;
|
temporaryAccessStartTime?: string;
|
||||||
permissions: TProjectSpecificPrivilegePermission;
|
permissions: TProjectPermission[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateProjectUserPrivlegeDTO = {
|
export type TUpdateProjectUserPrivlegeDTO = {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const useCreateProjectRole = () => {
|
|||||||
mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => {
|
mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => {
|
||||||
const {
|
const {
|
||||||
data: { role }
|
data: { role }
|
||||||
} = await apiRequest.post(`/api/v2/workspace/${projectSlug}/roles`, dto);
|
} = await apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto);
|
||||||
return role;
|
return role;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { projectSlug }) => {
|
onSuccess: (_, { projectSlug }) => {
|
||||||
@@ -38,7 +38,7 @@ export const useUpdateProjectRole = () => {
|
|||||||
mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => {
|
mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => {
|
||||||
const {
|
const {
|
||||||
data: { role }
|
data: { role }
|
||||||
} = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/roles/${id}`, dto);
|
} = await apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto);
|
||||||
return role;
|
return role;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { projectSlug }) => {
|
onSuccess: (_, { projectSlug }) => {
|
||||||
@@ -53,7 +53,7 @@ export const useDeleteProjectRole = () => {
|
|||||||
mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => {
|
mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => {
|
||||||
const {
|
const {
|
||||||
data: { role }
|
data: { role }
|
||||||
} = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/roles/${id}`);
|
} = await apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`);
|
||||||
return role;
|
return role;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { projectSlug }) => {
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ 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";
|
||||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { groupBy } from "@app/lib/fn/array";
|
|
||||||
import { omit } from "@app/lib/fn/object";
|
|
||||||
|
|
||||||
import { OrgUser, TProjectMembership } from "../users/types";
|
import { OrgUser, TProjectMembership } from "../users/types";
|
||||||
import {
|
import {
|
||||||
@@ -51,7 +49,7 @@ export const roleQueryKeys = {
|
|||||||
|
|
||||||
export const getProjectRoles = async (projectId: string) => {
|
export const getProjectRoles = async (projectId: string) => {
|
||||||
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
|
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
|
||||||
`/api/v2/workspace/${projectId}/roles`
|
`/api/v1/workspace/${projectId}/roles`
|
||||||
);
|
);
|
||||||
return data.roles;
|
return data.roles;
|
||||||
};
|
};
|
||||||
@@ -68,7 +66,7 @@ export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) =
|
|||||||
queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug),
|
queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ role: TProjectRole }>(
|
const { data } = await apiRequest.get<{ role: TProjectRole }>(
|
||||||
`/api/v2/workspace/${projectSlug}/roles/slug/${roleSlug}`
|
`/api/v1/workspace/${projectSlug}/roles/slug/${roleSlug}`
|
||||||
);
|
);
|
||||||
return data.role;
|
return data.role;
|
||||||
},
|
},
|
||||||
@@ -136,7 +134,7 @@ const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermiss
|
|||||||
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
||||||
membership: Omit<TProjectMembership, "roles"> & { roles: { role: string }[] };
|
membership: Omit<TProjectMembership, "roles"> & { roles: { role: string }[] };
|
||||||
};
|
};
|
||||||
}>(`/api/v2/workspace/${workspaceId}/permissions`, {});
|
}>(`/api/v1/workspace/${workspaceId}/permissions`, {});
|
||||||
|
|
||||||
return data.data;
|
return data.data;
|
||||||
};
|
};
|
||||||
@@ -148,32 +146,8 @@ export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPer
|
|||||||
enabled: Boolean(workspaceId),
|
enabled: Boolean(workspaceId),
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
|
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
|
||||||
const negatedRules = groupBy(
|
const ability = createMongoAbility<ProjectPermissionSet>(rule, { conditionsMatcher });
|
||||||
rule.filter((i) => i.inverted && i.conditions),
|
|
||||||
(i) => `${i.subject}-${JSON.stringify(i.conditions)}`
|
|
||||||
);
|
|
||||||
const ability = createMongoAbility<ProjectPermissionSet>(rule, {
|
|
||||||
// this allows in frontend to skip some rules using *
|
|
||||||
conditionsMatcher: (rules) => {
|
|
||||||
return (entity) => {
|
|
||||||
// skip validation if its negated rules
|
|
||||||
const isNegatedRule =
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
negatedRules?.[`${entity.__caslSubjectType__}-${JSON.stringify(rules)}`];
|
|
||||||
if (isNegatedRule) {
|
|
||||||
const baseMatcher = conditionsMatcher(rules);
|
|
||||||
return baseMatcher(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rulesStrippedOfWildcard = omit(
|
|
||||||
rules,
|
|
||||||
Object.keys(entity).filter((el) => entity[el]?.includes("*"))
|
|
||||||
);
|
|
||||||
const baseMatcher = conditionsMatcher(rulesStrippedOfWildcard);
|
|
||||||
return baseMatcher(entity);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const membership = {
|
const membership = {
|
||||||
...data.membership,
|
...data.membership,
|
||||||
roles: data.membership.roles.map(({ role }) => role)
|
roles: data.membership.roles.map(({ role }) => role)
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export type TPermission = {
|
|||||||
|
|
||||||
export type TProjectPermission = {
|
export type TProjectPermission = {
|
||||||
conditions?: Record<string, any>;
|
conditions?: Record<string, any>;
|
||||||
inverted?: boolean;
|
|
||||||
action: string | string[];
|
action: string | string[];
|
||||||
subject: string | string[];
|
subject: string | string[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,22 +13,3 @@ export const groupBy = <T, Key extends string | number | symbol>(
|
|||||||
acc[groupId].push(item);
|
acc[groupId].push(item);
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<Key, T[]>);
|
}, {} as Record<Key, T[]>);
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a list of items returns a new list with only
|
|
||||||
* unique items. Accepts an optional identity function
|
|
||||||
* to convert each item in the list to a comparable identity
|
|
||||||
* value
|
|
||||||
*/
|
|
||||||
export const unique = <T, K extends string | number | symbol>(
|
|
||||||
array: readonly T[],
|
|
||||||
toKey?: (item: T) => K
|
|
||||||
): T[] => {
|
|
||||||
const valueMap = array.reduce((acc, item) => {
|
|
||||||
const key = toKey ? toKey(item) : (item as unknown as string | number | symbol);
|
|
||||||
if (acc[key]) return acc;
|
|
||||||
acc[key] = item;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string | number | symbol, T>);
|
|
||||||
return Object.values(valueMap);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Omit a list of properties from an object
|
|
||||||
* returning a new object with the properties
|
|
||||||
* that remain
|
|
||||||
*/
|
|
||||||
export const omit = <T, TKeys extends keyof T>(obj: T, keys: TKeys[]): Omit<T, TKeys> => {
|
|
||||||
if (!obj) return {} as Omit<T, TKeys>;
|
|
||||||
if (!keys || keys.length === 0) return obj as Omit<T, TKeys>;
|
|
||||||
return keys.reduce(
|
|
||||||
(acc, key) => {
|
|
||||||
// Gross, I know, it's mutating the object, but we
|
|
||||||
// are allowing it in this very limited scope due
|
|
||||||
// to the performance implications of an omit func.
|
|
||||||
// Not a pattern or practice to use elsewhere.
|
|
||||||
delete acc[key];
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ ...obj }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+16
-14
@@ -184,20 +184,20 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
||||||
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
|
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
|
||||||
];
|
];
|
||||||
const conditions: { environment: string; secretPath?: { $glob: string } } = {
|
const conditions: Record<string, any> = { environment: data.environmentSlug };
|
||||||
environment: data.environmentSlug
|
|
||||||
};
|
|
||||||
if (data.secretPath) {
|
if (data.secretPath) {
|
||||||
conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) };
|
conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) };
|
||||||
}
|
}
|
||||||
await updateUserPrivilege.mutateAsync({
|
await updateUserPrivilege.mutateAsync({
|
||||||
privilegeId: privilege.id,
|
privilegeId: privilege.id,
|
||||||
...data.temporaryAccess,
|
...data.temporaryAccess,
|
||||||
permissions: {
|
permissions: actions
|
||||||
subject: ProjectPermissionSub.Secrets,
|
.filter(({ allowed }) => allowed)
|
||||||
conditions,
|
.map(({ action }) => ({
|
||||||
actions: actions.filter((i) => i.allowed).map((i) => i.action)
|
action,
|
||||||
},
|
subject: [ProjectPermissionSub.Secrets],
|
||||||
|
conditions
|
||||||
|
})),
|
||||||
projectMembershipId: privilege.projectMembershipId
|
projectMembershipId: privilege.projectMembershipId
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -642,13 +642,15 @@ export const SpecificPrivilegeSection = ({ membershipId }: Props) => {
|
|||||||
if (createUserPrivilege.isLoading) return;
|
if (createUserPrivilege.isLoading) return;
|
||||||
try {
|
try {
|
||||||
await createUserPrivilege.mutateAsync({
|
await createUserPrivilege.mutateAsync({
|
||||||
permissions: {
|
permissions: [
|
||||||
actions: [ProjectPermissionActions.Read],
|
{
|
||||||
subject: ProjectPermissionSub.Secrets,
|
action: ProjectPermissionActions.Read,
|
||||||
conditions: {
|
subject: [ProjectPermissionSub.Secrets],
|
||||||
environment: currentWorkspace?.environments?.[0].slug || ""
|
conditions: {
|
||||||
|
environment: currentWorkspace?.environments?.[0].slug
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
],
|
||||||
projectMembershipId: membershipId
|
projectMembershipId: membershipId
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
|
|||||||
+53
-155
@@ -7,7 +7,6 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import {
|
import {
|
||||||
PermissionConditionOperators,
|
PermissionConditionOperators,
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
TPermissionCondition,
|
TPermissionCondition,
|
||||||
TPermissionConditionOperators
|
TPermissionConditionOperators
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
@@ -29,12 +28,8 @@ const CmekPolicyActionSchema = z.object({
|
|||||||
decrypt: z.boolean().optional()
|
decrypt: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const DynamicSecretPolicyActionSchema = z.object({
|
const SecretFolderPolicyActionSchema = z.object({
|
||||||
[ProjectPermissionDynamicSecretActions.ReadRootCredential]: z.boolean().optional(),
|
read: z.boolean().optional()
|
||||||
[ProjectPermissionDynamicSecretActions.EditRootCredential]: z.boolean().optional(),
|
|
||||||
[ProjectPermissionDynamicSecretActions.DeleteRootCredential]: z.boolean().optional(),
|
|
||||||
[ProjectPermissionDynamicSecretActions.CreateRootCredential]: z.boolean().optional(),
|
|
||||||
[ProjectPermissionDynamicSecretActions.Lease]: z.boolean().optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const SecretRollbackPolicyActionSchema = z.object({
|
const SecretRollbackPolicyActionSchema = z.object({
|
||||||
@@ -47,29 +42,11 @@ const WorkspacePolicyActionSchema = z.object({
|
|||||||
delete: z.boolean().optional()
|
delete: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const ConditionSchema = z
|
const ConditionSchema = z.object({
|
||||||
.object({
|
operator: z.string(),
|
||||||
operator: z.string(),
|
lhs: z.string(),
|
||||||
lhs: z.string(),
|
rhs: z.string().min(1)
|
||||||
rhs: z.string().min(1)
|
});
|
||||||
})
|
|
||||||
.array()
|
|
||||||
.optional()
|
|
||||||
.default([])
|
|
||||||
.refine(
|
|
||||||
(el) => {
|
|
||||||
const lhsOperatorSet = new Set<string>();
|
|
||||||
for (let i = 0; i < el.length; i += 1) {
|
|
||||||
const { lhs, operator } = el[i];
|
|
||||||
if (lhsOperatorSet.has(`${lhs}-${operator}`)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
lhsOperatorSet.add(`${lhs}-${operator}`);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
{ message: "Duplicate operator found for a condition" }
|
|
||||||
);
|
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
@@ -82,29 +59,27 @@ export const formSchema = z.object({
|
|||||||
permissions: z
|
permissions: z
|
||||||
.object({
|
.object({
|
||||||
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
||||||
inverted: z.boolean().optional(),
|
conditions: ConditionSchema.array()
|
||||||
conditions: ConditionSchema
|
.optional()
|
||||||
})
|
.default([])
|
||||||
.array()
|
.refine(
|
||||||
.default([]),
|
(el) => {
|
||||||
[ProjectPermissionSub.SecretFolders]: GeneralPolicyActionSchema.extend({
|
const lhsOperatorSet = new Set<string>();
|
||||||
inverted: z.boolean().optional(),
|
for (let i = 0; i < el.length; i += 1) {
|
||||||
conditions: ConditionSchema
|
const { lhs, operator } = el[i];
|
||||||
})
|
if (lhsOperatorSet.has(`${lhs}-${operator}`)) {
|
||||||
.array()
|
return false;
|
||||||
.default([]),
|
}
|
||||||
[ProjectPermissionSub.SecretImports]: GeneralPolicyActionSchema.extend({
|
lhsOperatorSet.add(`${lhs}-${operator}`);
|
||||||
inverted: z.boolean().optional(),
|
}
|
||||||
conditions: ConditionSchema
|
return true;
|
||||||
})
|
},
|
||||||
.array()
|
{ message: "Duplicate operator found for a condition" }
|
||||||
.default([]),
|
)
|
||||||
[ProjectPermissionSub.DynamicSecrets]: DynamicSecretPolicyActionSchema.extend({
|
|
||||||
inverted: z.boolean().optional(),
|
|
||||||
conditions: ConditionSchema
|
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
|
[ProjectPermissionSub.SecretFolders]: SecretFolderPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]),
|
||||||
@@ -123,7 +98,7 @@ export const formSchema = z.object({
|
|||||||
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]),
|
||||||
@@ -135,22 +110,8 @@ export const formSchema = z.object({
|
|||||||
|
|
||||||
export type TFormSchema = z.infer<typeof formSchema>;
|
export type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
type TConditionalFields =
|
|
||||||
| ProjectPermissionSub.Secrets
|
|
||||||
| ProjectPermissionSub.SecretFolders
|
|
||||||
| ProjectPermissionSub.SecretImports
|
|
||||||
| ProjectPermissionSub.DynamicSecrets;
|
|
||||||
|
|
||||||
export const isConditionalSubjects = (
|
|
||||||
subject: ProjectPermissionSub
|
|
||||||
): subject is TConditionalFields =>
|
|
||||||
subject === (ProjectPermissionSub.Secrets as const) ||
|
|
||||||
subject === ProjectPermissionSub.DynamicSecrets ||
|
|
||||||
subject === ProjectPermissionSub.SecretImports ||
|
|
||||||
subject === ProjectPermissionSub.SecretFolders;
|
|
||||||
|
|
||||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||||
const formConditions: z.infer<typeof ConditionSchema> = [];
|
const formConditions: z.infer<typeof ConditionSchema>[] = [];
|
||||||
Object.entries(caslConditions).forEach(([type, condition]) => {
|
Object.entries(caslConditions).forEach(([type, condition]) => {
|
||||||
if (typeof condition === "string") {
|
if (typeof condition === "string") {
|
||||||
formConditions.push({
|
formConditions.push({
|
||||||
@@ -177,15 +138,12 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
const formVal: Partial<TFormSchema["permissions"]> = {};
|
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||||
|
|
||||||
permissions.forEach((permission) => {
|
permissions.forEach((permission) => {
|
||||||
const { subject: caslSub, action, conditions, inverted } = permission;
|
const { subject: caslSub, action, conditions } = permission;
|
||||||
const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub;
|
const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
ProjectPermissionSub.Secrets,
|
ProjectPermissionSub.Secrets,
|
||||||
ProjectPermissionSub.DynamicSecrets,
|
|
||||||
ProjectPermissionSub.SecretFolders,
|
|
||||||
ProjectPermissionSub.SecretImports,
|
|
||||||
ProjectPermissionSub.Member,
|
ProjectPermissionSub.Member,
|
||||||
ProjectPermissionSub.Groups,
|
ProjectPermissionSub.Groups,
|
||||||
ProjectPermissionSub.Identity,
|
ProjectPermissionSub.Identity,
|
||||||
@@ -208,67 +166,37 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
ProjectPermissionSub.Kms
|
ProjectPermissionSub.Kms
|
||||||
].includes(subject)
|
].includes(subject)
|
||||||
) {
|
) {
|
||||||
|
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||||
|
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||||
|
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||||
|
const canCreate = action.includes(ProjectPermissionActions.Create);
|
||||||
|
|
||||||
// from above statement we are sure it won't be undefined
|
// from above statement we are sure it won't be undefined
|
||||||
if (isConditionalSubjects(subject)) {
|
if (subject === ProjectPermissionSub.Secrets) {
|
||||||
if (!formVal[subject]) formVal[subject] = [];
|
if (!formVal[subject]) formVal[subject] = [];
|
||||||
|
formVal[subject]!.push({
|
||||||
if (subject === ProjectPermissionSub.DynamicSecrets) {
|
read: canRead,
|
||||||
const canRead = action.includes(ProjectPermissionDynamicSecretActions.ReadRootCredential);
|
create: canCreate,
|
||||||
const canEdit = action.includes(ProjectPermissionDynamicSecretActions.EditRootCredential);
|
edit: canEdit,
|
||||||
const canDelete = action.includes(
|
delete: canDelete,
|
||||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential
|
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : []
|
||||||
);
|
});
|
||||||
const canCreate = action.includes(
|
|
||||||
ProjectPermissionDynamicSecretActions.CreateRootCredential
|
|
||||||
);
|
|
||||||
const canLease = action.includes(ProjectPermissionDynamicSecretActions.Lease);
|
|
||||||
|
|
||||||
// from above statement we are sure it won't be undefined
|
|
||||||
formVal[subject]!.push({
|
|
||||||
[ProjectPermissionDynamicSecretActions.ReadRootCredential]: canRead,
|
|
||||||
[ProjectPermissionDynamicSecretActions.CreateRootCredential]: canCreate,
|
|
||||||
[ProjectPermissionDynamicSecretActions.EditRootCredential]: canEdit,
|
|
||||||
[ProjectPermissionDynamicSecretActions.DeleteRootCredential]: canDelete,
|
|
||||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
|
||||||
inverted,
|
|
||||||
[ProjectPermissionDynamicSecretActions.Lease]: canLease
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// for other subjects
|
|
||||||
const canRead = action.includes(ProjectPermissionActions.Read);
|
|
||||||
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
|
||||||
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
|
||||||
const canCreate = action.includes(ProjectPermissionActions.Create);
|
|
||||||
formVal[subject]!.push({
|
|
||||||
read: canRead,
|
|
||||||
create: canCreate,
|
|
||||||
edit: canEdit,
|
|
||||||
delete: canDelete,
|
|
||||||
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
|
||||||
inverted
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// deduplicate multiple rules for other policies
|
// deduplicate multiple rules for other policies
|
||||||
// because they don't have condition it doesn't make sense for multiple rules
|
// because they don't have condition it doesn't make sense for multiple rules
|
||||||
const canRead = action.includes(ProjectPermissionActions.Read);
|
|
||||||
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
|
||||||
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
|
||||||
const canCreate = action.includes(ProjectPermissionActions.Create);
|
|
||||||
|
|
||||||
if (!formVal[subject]) formVal[subject] = [{}];
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||||
if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true;
|
if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true;
|
||||||
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
||||||
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
|
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
|
||||||
}
|
}
|
||||||
} else if (subject === ProjectPermissionSub.Project) {
|
} else if (subject === ProjectPermissionSub.Workspace) {
|
||||||
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||||
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||||
if (!formVal[subject]) formVal[subject] = [{}];
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
|
||||||
// from above statement we are sure it won't be undefined
|
// from above statement we are sure it won't be undefined
|
||||||
if (canEdit) formVal[subject as ProjectPermissionSub.Project]![0].edit = true;
|
if (canEdit) formVal[subject as ProjectPermissionSub.Workspace]![0].edit = true;
|
||||||
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
|
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
|
||||||
} else if (subject === ProjectPermissionSub.SecretRollback) {
|
} else if (subject === ProjectPermissionSub.SecretRollback) {
|
||||||
const canRead = action.includes(ProjectPermissionActions.Read);
|
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||||
@@ -278,6 +206,12 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
// from above statement we are sure it won't be undefined
|
// from above statement we are sure it won't be undefined
|
||||||
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||||
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
||||||
|
} else if (subject === ProjectPermissionSub.SecretFolders) {
|
||||||
|
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||||
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
|
||||||
|
// from above statement we are sure it won't be undefined
|
||||||
|
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||||
} else if (subject === ProjectPermissionSub.Cmek) {
|
} else if (subject === ProjectPermissionSub.Cmek) {
|
||||||
const canRead = action.includes(ProjectPermissionCmekActions.Read);
|
const canRead = action.includes(ProjectPermissionCmekActions.Read);
|
||||||
const canEdit = action.includes(ProjectPermissionCmekActions.Edit);
|
const canEdit = action.includes(ProjectPermissionCmekActions.Edit);
|
||||||
@@ -330,7 +264,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
|||||||
Object.entries(formVal || {}).forEach(([subject, rules]) => {
|
Object.entries(formVal || {}).forEach(([subject, rules]) => {
|
||||||
rules.forEach((actions) => {
|
rules.forEach((actions) => {
|
||||||
const caslActions = Object.keys(actions).filter(
|
const caslActions = Object.keys(actions).filter(
|
||||||
(el) => actions?.[el as keyof typeof actions] && el !== "conditions" && el !== "inverted"
|
(el) => actions?.[el as keyof typeof actions] && el !== "conditions"
|
||||||
);
|
);
|
||||||
const caslConditions =
|
const caslConditions =
|
||||||
"conditions" in actions
|
"conditions" in actions
|
||||||
@@ -340,7 +274,6 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
|||||||
permissions.push({
|
permissions.push({
|
||||||
action: caslActions,
|
action: caslActions,
|
||||||
subject,
|
subject,
|
||||||
inverted: (actions as { inverted?: boolean })?.inverted,
|
|
||||||
conditions: caslConditions
|
conditions: caslConditions
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -355,7 +288,7 @@ export type TProjectPermissionObject = {
|
|||||||
label: string;
|
label: string;
|
||||||
value: keyof Omit<
|
value: keyof Omit<
|
||||||
NonNullable<NonNullable<TFormSchema["permissions"]>[K]>[number],
|
NonNullable<NonNullable<TFormSchema["permissions"]>[K]>[number],
|
||||||
"conditions" | "inverted"
|
"conditions"
|
||||||
>;
|
>;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
@@ -373,42 +306,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
},
|
},
|
||||||
[ProjectPermissionSub.SecretFolders]: {
|
[ProjectPermissionSub.SecretFolders]: {
|
||||||
title: "Secret Folders",
|
title: "Secret Folders",
|
||||||
actions: [
|
actions: [{ label: "Read Only", value: "read" }]
|
||||||
{ label: "Create", value: "create" },
|
|
||||||
{ label: "Modify", value: "edit" },
|
|
||||||
{ label: "Remove", value: "delete" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
[ProjectPermissionSub.SecretImports]: {
|
|
||||||
title: "Secret Imports",
|
|
||||||
actions: [
|
|
||||||
{ label: "Read", value: "read" },
|
|
||||||
{ label: "Create", value: "create" },
|
|
||||||
{ label: "Modify", value: "edit" },
|
|
||||||
{ label: "Remove", value: "delete" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
[ProjectPermissionSub.DynamicSecrets]: {
|
|
||||||
title: "Dynamic Secrets",
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: "Read root credentials",
|
|
||||||
value: ProjectPermissionDynamicSecretActions.ReadRootCredential
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Create root credentials",
|
|
||||||
value: ProjectPermissionDynamicSecretActions.CreateRootCredential
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Modify root credentials",
|
|
||||||
value: ProjectPermissionDynamicSecretActions.EditRootCredential
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Remove root credentials",
|
|
||||||
value: ProjectPermissionDynamicSecretActions.DeleteRootCredential
|
|
||||||
},
|
|
||||||
{ label: "Manage Leases", value: ProjectPermissionDynamicSecretActions.Lease }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
[ProjectPermissionSub.Cmek]: {
|
[ProjectPermissionSub.Cmek]: {
|
||||||
title: "KMS",
|
title: "KMS",
|
||||||
@@ -434,7 +332,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
{ label: "Remove", value: "delete" }
|
{ label: "Remove", value: "delete" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[ProjectPermissionSub.Project]: {
|
[ProjectPermissionSub.Workspace]: {
|
||||||
title: "Project",
|
title: "Project",
|
||||||
actions: [
|
actions: [
|
||||||
{ label: "Update project details", value: "edit" },
|
{ label: "Update project details", value: "edit" },
|
||||||
|
|||||||
+6
-17
@@ -10,15 +10,13 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
|||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||||
|
|
||||||
import { GeneralPermissionConditions } from "./components/GeneralPermissionConditions";
|
import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions";
|
||||||
import { GeneralPermissionPolicies } from "./components/GeneralPermissionPolicies";
|
|
||||||
import { NewPermissionRule } from "./components/NewPermissionRule";
|
import { NewPermissionRule } from "./components/NewPermissionRule";
|
||||||
import { SecretPermissionConditions } from "./components/SecretPermissionConditions";
|
import { SecretPermissionConditions } from "./components/SecretPermissionConditions";
|
||||||
import { PermissionEmptyState } from "./PermissionEmptyState";
|
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||||
import {
|
import {
|
||||||
formRolePermission2API,
|
formRolePermission2API,
|
||||||
formSchema,
|
formSchema,
|
||||||
isConditionalSubjects,
|
|
||||||
PROJECT_PERMISSION_OBJECT,
|
PROJECT_PERMISSION_OBJECT,
|
||||||
rolePermission2Form,
|
rolePermission2Form,
|
||||||
TFormSchema
|
TFormSchema
|
||||||
@@ -29,17 +27,6 @@ type Props = {
|
|||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderConditionalComponents = (subject: ProjectPermissionSub, isDisabled?: boolean) => {
|
|
||||||
if (subject === ProjectPermissionSub.Secrets)
|
|
||||||
return <SecretPermissionConditions isDisabled={isDisabled} />;
|
|
||||||
|
|
||||||
if (isConditionalSubjects(subject)) {
|
|
||||||
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
||||||
@@ -143,15 +130,17 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
{!isLoading && <PermissionEmptyState />}
|
{!isLoading && <PermissionEmptyState />}
|
||||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||||
<GeneralPermissionPolicies
|
<GeneralPermissionOptions
|
||||||
subject={subject}
|
subject={subject}
|
||||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||||
key={`project-permission-${subject}`}
|
key={`project-permission-${subject}`}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
>
|
>
|
||||||
{renderConditionalComponents(subject, isDisabled)}
|
{subject === ProjectPermissionSub.Secrets ? (
|
||||||
</GeneralPermissionPolicies>
|
<SecretPermissionConditions isDisabled={isDisabled} />
|
||||||
|
) : undefined}
|
||||||
|
</GeneralPermissionOptions>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|||||||
-176
@@ -1,176 +0,0 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
PermissionConditionOperators,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
|
||||||
|
|
||||||
import { TFormSchema } from "../ProjectRoleModifySection.utils";
|
|
||||||
import {
|
|
||||||
getConditionOperatorHelperInfo,
|
|
||||||
renderOperatorSelectItems
|
|
||||||
} from "./PermissionConditionHelpers";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
position?: number;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
type:
|
|
||||||
| ProjectPermissionSub.DynamicSecrets
|
|
||||||
| ProjectPermissionSub.SecretFolders
|
|
||||||
| ProjectPermissionSub.SecretImports;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
formState: { errors }
|
|
||||||
} = useFormContext<TFormSchema>();
|
|
||||||
const items = useFieldArray({
|
|
||||||
control,
|
|
||||||
name: `permissions.${type}.${position}.conditions`
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
|
||||||
<p className="mb-2 text-sm text-mineshaft-400">
|
|
||||||
When this policy should apply (always if no conditions are added).
|
|
||||||
</p>
|
|
||||||
<div className="mt-2 flex flex-col space-y-2">
|
|
||||||
{items.fields.map((el, index) => {
|
|
||||||
const condition =
|
|
||||||
(watch(`permissions.${type}.${position}.conditions.${index}`) as {
|
|
||||||
lhs: string;
|
|
||||||
rhs: string;
|
|
||||||
operator: string;
|
|
||||||
}) || {};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={el.id}
|
|
||||||
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
|
||||||
>
|
|
||||||
<div className="w-1/4">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`permissions.${type}.${position}.conditions.${index}.lhs`}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-0"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => field.onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<SelectItem value="environment">Environment Slug</SelectItem>
|
|
||||||
<SelectItem value="secretPath">Secret Path</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-36 items-center space-x-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`permissions.${type}.${position}.conditions.${index}.operator`}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => field.onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{renderOperatorSelectItems(condition.lhs)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Tooltip
|
|
||||||
asChild
|
|
||||||
content={getConditionOperatorHelperInfo(
|
|
||||||
condition?.operator as PermissionConditionOperators
|
|
||||||
)}
|
|
||||||
className="max-w-xs"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`permissions.${type}.${position}.conditions.${index}.rhs`}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="plus"
|
|
||||||
variant="outline_bg"
|
|
||||||
className="p-2.5"
|
|
||||||
onClick={() => items.remove(index)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{errors?.permissions?.[type]?.[position]?.conditions?.message && (
|
|
||||||
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
|
||||||
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
|
||||||
<span>{errors?.permissions?.[type]?.[position]?.conditions?.message}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>{}</div>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
variant="star"
|
|
||||||
size="xs"
|
|
||||||
className="mt-3"
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
onClick={() =>
|
|
||||||
items.append({
|
|
||||||
lhs: "environment",
|
|
||||||
operator: PermissionConditionOperators.$EQ,
|
|
||||||
rhs: ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Add Condition
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+6
-54
@@ -1,24 +1,14 @@
|
|||||||
import { cloneElement } from "react";
|
import { cloneElement } from "react";
|
||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
import {
|
import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
faChevronDown,
|
|
||||||
faChevronRight,
|
|
||||||
faInfoCircle,
|
|
||||||
faPlus,
|
|
||||||
faTrash
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Button, Checkbox, Select, SelectItem, Tag, Tooltip } from "@app/components/v2";
|
import { Button, Checkbox, Tag } from "@app/components/v2";
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
import {
|
import { TFormSchema, TProjectPermissionObject } from "../ProjectRoleModifySection.utils";
|
||||||
isConditionalSubjects,
|
|
||||||
TFormSchema,
|
|
||||||
TProjectPermissionObject
|
|
||||||
} from "../ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props<T extends ProjectPermissionSub> = {
|
type Props<T extends ProjectPermissionSub> = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -28,7 +18,7 @@ type Props<T extends ProjectPermissionSub> = {
|
|||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
|
export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
|
||||||
subject,
|
subject,
|
||||||
actions,
|
actions,
|
||||||
children,
|
children,
|
||||||
@@ -73,44 +63,6 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
<div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6">
|
<div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6">
|
||||||
{items.fields.map((el, rootIndex) => (
|
{items.fields.map((el, rootIndex) => (
|
||||||
<div key={el.id} className="bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md">
|
<div key={el.id} className="bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md">
|
||||||
{isConditionalSubjects(subject) && (
|
|
||||||
<div className="mt-4 mb-6 flex w-full items-center text-gray-300">
|
|
||||||
<div className="w-1/4">Permission</div>
|
|
||||||
<div className="mr-4 w-1/4">
|
|
||||||
<Controller
|
|
||||||
defaultValue={false as any}
|
|
||||||
name={`permissions.${subject}.${rootIndex}.inverted`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
value={String(field.value)}
|
|
||||||
onValueChange={(val) => field.onChange(val === "true")}
|
|
||||||
containerClassName="w-full"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<SelectItem value="false">Allow</SelectItem>
|
|
||||||
<SelectItem value="true">Forbid</SelectItem>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Tooltip
|
|
||||||
asChild
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
Whether to allow or forbid the selected actions when the following
|
|
||||||
conditions (if any) are met.
|
|
||||||
</p>
|
|
||||||
<p className="mt-2">Forbid rules must come after allow rules.</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="text-gray-400" />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex text-gray-300">
|
<div className="flex text-gray-300">
|
||||||
<div className="w-1/4">Actions</div>
|
<div className="w-1/4">Actions</div>
|
||||||
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||||
@@ -146,10 +98,10 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
|
|||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"mt-4 flex justify-start space-x-4",
|
"mt-4 flex justify-start space-x-4",
|
||||||
isConditionalSubjects(subject) && "justify-end"
|
subject === ProjectPermissionSub.Secrets && "justify-end"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isDisabled && isConditionalSubjects(subject) && (
|
{!isDisabled && subject === ProjectPermissionSub.Secrets && (
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
variant="star"
|
variant="star"
|
||||||
+1
-2
@@ -15,7 +15,6 @@ import { ProjectPermissionSub } from "@app/context";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
formSchema,
|
formSchema,
|
||||||
isConditionalSubjects,
|
|
||||||
PROJECT_PERMISSION_OBJECT,
|
PROJECT_PERMISSION_OBJECT,
|
||||||
TFormSchema
|
TFormSchema
|
||||||
} from "../ProjectRoleModifySection.utils";
|
} from "../ProjectRoleModifySection.utils";
|
||||||
@@ -90,7 +89,7 @@ export const NewPermissionRule = ({ onClose }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={form.handleSubmit((el) => {
|
onClick={form.handleSubmit((el) => {
|
||||||
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
|
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
|
||||||
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
|
if (rootPolicyValue && selectedSubject === ProjectPermissionSub.Secrets) {
|
||||||
rootForm.setValue(
|
rootForm.setValue(
|
||||||
`permissions.${el.type}`,
|
`permissions.${el.type}`,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
import { GlobPermissionInfo } from "@app/components/permissions";
|
|
||||||
import { SelectItem } from "@app/components/v2";
|
|
||||||
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
|
|
||||||
|
|
||||||
export const getConditionOperatorHelperInfo = (type: PermissionConditionOperators) => {
|
|
||||||
switch (type) {
|
|
||||||
case PermissionConditionOperators.$EQ:
|
|
||||||
return "Value should equal specified value.";
|
|
||||||
case PermissionConditionOperators.$NEQ:
|
|
||||||
return "Value should not equal specified value.";
|
|
||||||
case PermissionConditionOperators.$IN:
|
|
||||||
return "List of comma-separated values that match a given value.";
|
|
||||||
case PermissionConditionOperators.$GLOB:
|
|
||||||
return <GlobPermissionInfo />;
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const renderOperatorSelectItems = (type: string) => {
|
|
||||||
if (type === "secretTags") {
|
|
||||||
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
|
|
||||||
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
|
|
||||||
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
|
|
||||||
<SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+21
-49
@@ -1,34 +1,27 @@
|
|||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
|
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
import { TFormSchema } from "../ProjectRoleModifySection.utils";
|
import { TFormSchema } from "../ProjectRoleModifySection.utils";
|
||||||
import {
|
|
||||||
getConditionOperatorHelperInfo,
|
|
||||||
renderOperatorSelectItems
|
|
||||||
} from "./PermissionConditionHelpers";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
position?: number;
|
position?: number;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getValueLabel = (type: string) => {
|
||||||
|
if (type === "environment") return "Environment slug";
|
||||||
|
if (type === "secretPath") return "Folder path";
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
} = useFormContext<TFormSchema>();
|
} = useFormContext<TFormSchema>();
|
||||||
const items = useFieldArray({
|
const items = useFieldArray({
|
||||||
@@ -37,18 +30,10 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
<div className="mt-6 border-t border-t-gray-800 bg-mineshaft-800 pt-2">
|
||||||
<p className="mt-2 text-gray-300">Conditions</p>
|
|
||||||
<p className="mb-2 text-sm text-mineshaft-400">
|
|
||||||
When this policy should apply (always if no conditions are added).
|
|
||||||
</p>
|
|
||||||
<div className="mt-2 flex flex-col space-y-2">
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
{items.fields.map((el, index) => {
|
{items.fields.map((el, index) => {
|
||||||
const condition = watch(`permissions.secrets.${position}.conditions.${index}`) as {
|
const lhs = watch(`permissions.secrets.${position}.conditions.${index}.lhs`);
|
||||||
lhs: string;
|
|
||||||
rhs: string;
|
|
||||||
operator: string;
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={el.id}
|
key={el.id}
|
||||||
@@ -67,25 +52,17 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
|||||||
<Select
|
<Select
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
{...field}
|
{...field}
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => field.onChange(e)}
|
||||||
setValue(
|
|
||||||
`permissions.secrets.${position}.conditions.${index}.operator`,
|
|
||||||
PermissionConditionOperators.$IN as never
|
|
||||||
);
|
|
||||||
field.onChange(e);
|
|
||||||
}}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<SelectItem value="environment">Environment Slug</SelectItem>
|
<SelectItem value="environment">Environment Slug</SelectItem>
|
||||||
<SelectItem value="secretPath">Secret Path</SelectItem>
|
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||||
<SelectItem value="secretName">Secret Name</SelectItem>
|
|
||||||
<SelectItem value="secretTags">Secret Tags</SelectItem>
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-36 items-center space-x-2">
|
<div className="w-36">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={`permissions.secrets.${position}.conditions.${index}.operator`}
|
name={`permissions.secrets.${position}.conditions.${index}.operator`}
|
||||||
@@ -101,22 +78,16 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
|||||||
onValueChange={(e) => field.onChange(e)}
|
onValueChange={(e) => field.onChange(e)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{renderOperatorSelectItems(condition.lhs)}
|
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$GLOB}>
|
||||||
|
Glob Match
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<Tooltip
|
|
||||||
asChild
|
|
||||||
content={getConditionOperatorHelperInfo(
|
|
||||||
condition?.operator as PermissionConditionOperators
|
|
||||||
)}
|
|
||||||
className="max-w-xs"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<Controller
|
<Controller
|
||||||
@@ -128,7 +99,7 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
className="mb-0 flex-grow"
|
className="mb-0 flex-grow"
|
||||||
>
|
>
|
||||||
<Input {...field} />
|
<Input {...field} placeholder={getValueLabel(lhs)} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -153,6 +124,7 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
|||||||
<span>{errors?.permissions?.secrets?.[position]?.conditions?.message}</span>
|
<span>{errors?.permissions?.secrets?.[position]?.conditions?.message}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div>{}</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
@@ -168,7 +140,7 @@ export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Add Condition
|
New Condition
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { PermissionDeniedBanner } from "@app/components/permissions";
|
|||||||
import { ContentLoader, Pagination } from "@app/components/v2";
|
import { ContentLoader, Pagination } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
@@ -38,7 +37,7 @@ import { ActionBar } from "./components/ActionBar";
|
|||||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||||
import { PitDrawer } from "./components/PitDrawer";
|
import { PitDrawer } from "./components/PitDrawer";
|
||||||
import { SecretDropzone } from "./components/SecretDropzone";
|
import { SecretDropzone } from "./components/SecretDropzone";
|
||||||
import { SecretListView, SecretNoAccessListView } from "./components/SecretListView";
|
import { SecretListView } from "./components/SecretListView";
|
||||||
import { SnapshotView } from "./components/SnapshotView";
|
import { SnapshotView } from "./components/SnapshotView";
|
||||||
import { StoreProvider } from "./SecretMainPage.store";
|
import { StoreProvider } from "./SecretMainPage.store";
|
||||||
import { Filter, RowType } from "./SecretMainPage.types";
|
import { Filter, RowType } from "./SecretMainPage.types";
|
||||||
@@ -80,24 +79,8 @@ export const SecretMainPage = () => {
|
|||||||
const secretPath = (router.query.secretPath as string) || "/";
|
const secretPath = (router.query.secretPath as string) || "/";
|
||||||
const canReadSecret = permission.can(
|
const canReadSecret = permission.can(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const canReadSecretImports = permission.can(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
|
||||||
);
|
|
||||||
|
|
||||||
const canReadDynamicSecret = permission.can(
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
|
|
||||||
);
|
|
||||||
|
|
||||||
const canDoReadRollback = permission.can(
|
const canDoReadRollback = permission.can(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
ProjectPermissionSub.SecretRollback
|
ProjectPermissionSub.SecretRollback
|
||||||
@@ -106,12 +89,11 @@ export const SecretMainPage = () => {
|
|||||||
const defaultFilterState = {
|
const defaultFilterState = {
|
||||||
tags: {},
|
tags: {},
|
||||||
searchFilter: (router.query.searchFilter as string) || "",
|
searchFilter: (router.query.searchFilter as string) || "",
|
||||||
// these should always be on by default for the UI, they will be disabled for the query below based off permissions
|
|
||||||
include: {
|
include: {
|
||||||
[RowType.Folder]: true,
|
[RowType.Folder]: true,
|
||||||
[RowType.Import]: true,
|
[RowType.Import]: canReadSecret,
|
||||||
[RowType.DynamicSecret]: true,
|
[RowType.DynamicSecret]: canReadSecret,
|
||||||
[RowType.Secret]: true
|
[RowType.Secret]: canReadSecret
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,6 +101,19 @@ export const SecretMainPage = () => {
|
|||||||
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
|
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(filter.searchFilter);
|
||||||
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
|
const [filterHistory, setFilterHistory] = useState<Map<string, Filter>>(new Map());
|
||||||
|
|
||||||
|
// change filters if permissions change at different paths/env
|
||||||
|
useEffect(() => {
|
||||||
|
setFilter((prev) => ({
|
||||||
|
...prev,
|
||||||
|
include: {
|
||||||
|
[RowType.Folder]: true,
|
||||||
|
[RowType.Import]: canReadSecret,
|
||||||
|
[RowType.DynamicSecret]: canReadSecret,
|
||||||
|
[RowType.Secret]: canReadSecret
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}, [canReadSecret]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isWorkspaceLoading &&
|
!isWorkspaceLoading &&
|
||||||
@@ -146,9 +141,9 @@ export const SecretMainPage = () => {
|
|||||||
orderBy,
|
orderBy,
|
||||||
search: debouncedSearchFilter,
|
search: debouncedSearchFilter,
|
||||||
orderDirection,
|
orderDirection,
|
||||||
includeImports: canReadSecretImports && filter.include.import,
|
includeImports: canReadSecret && filter.include.import,
|
||||||
includeFolders: filter.include.folder,
|
includeFolders: filter.include.folder,
|
||||||
includeDynamicSecrets: canReadDynamicSecret && filter.include.dynamic,
|
includeDynamicSecrets: canReadSecret && filter.include.dynamic,
|
||||||
includeSecrets: canReadSecret && filter.include.secret,
|
includeSecrets: canReadSecret && filter.include.secret,
|
||||||
tags: filter.tags
|
tags: filter.tags
|
||||||
});
|
});
|
||||||
@@ -210,20 +205,8 @@ export const SecretMainPage = () => {
|
|||||||
isPaused: !canDoReadRollback
|
isPaused: !canDoReadRollback
|
||||||
});
|
});
|
||||||
|
|
||||||
const noAccessSecretCount = Math.max(
|
|
||||||
(page * perPage > totalCount ? totalCount % perPage : perPage) -
|
|
||||||
(imports?.length || 0) -
|
|
||||||
(folders?.length || 0) -
|
|
||||||
(secrets?.length || 0) -
|
|
||||||
(dynamicSecrets?.length || 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const isNotEmpty = Boolean(
|
const isNotEmpty = Boolean(
|
||||||
secrets?.length ||
|
secrets?.length || folders?.length || imports?.length || dynamicSecrets?.length
|
||||||
folders?.length ||
|
|
||||||
imports?.length ||
|
|
||||||
dynamicSecrets?.length ||
|
|
||||||
noAccessSecretCount
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSortToggle = () =>
|
const handleSortToggle = () =>
|
||||||
@@ -315,6 +298,7 @@ export const SecretMainPage = () => {
|
|||||||
setFilter(defaultFilterState);
|
setFilter(defaultFilterState);
|
||||||
setDebouncedSearchFilter("");
|
setDebouncedSearchFilter("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoreProvider>
|
<StoreProvider>
|
||||||
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto flex flex-col px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
@@ -378,7 +362,7 @@ export const SecretMainPage = () => {
|
|||||||
<div className="flex-grow px-4 py-2">Value</div>
|
<div className="flex-grow px-4 py-2">Value</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{canReadSecretImports && Boolean(imports?.length) && (
|
{canReadSecret && imports?.length && (
|
||||||
<SecretImportListView
|
<SecretImportListView
|
||||||
searchTerm={debouncedSearchFilter}
|
searchTerm={debouncedSearchFilter}
|
||||||
secretImports={imports}
|
secretImports={imports}
|
||||||
@@ -389,7 +373,7 @@ export const SecretMainPage = () => {
|
|||||||
importedSecrets={importedSecrets}
|
importedSecrets={importedSecrets}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{Boolean(folders?.length) && (
|
{folders?.length && (
|
||||||
<FolderListView
|
<FolderListView
|
||||||
folders={folders}
|
folders={folders}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
@@ -398,7 +382,7 @@ export const SecretMainPage = () => {
|
|||||||
onNavigateToFolder={handleResetFilter}
|
onNavigateToFolder={handleResetFilter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadDynamicSecret && Boolean(dynamicSecrets?.length) && (
|
{canReadSecret && dynamicSecrets?.length && (
|
||||||
<DynamicSecretListView
|
<DynamicSecretListView
|
||||||
environment={environment}
|
environment={environment}
|
||||||
projectSlug={projectSlug}
|
projectSlug={projectSlug}
|
||||||
@@ -406,7 +390,7 @@ export const SecretMainPage = () => {
|
|||||||
dynamicSecrets={dynamicSecrets}
|
dynamicSecrets={dynamicSecrets}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadSecret && Boolean(secrets?.length) && (
|
{canReadSecret && secrets?.length && (
|
||||||
<SecretListView
|
<SecretListView
|
||||||
secrets={secrets}
|
secrets={secrets}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
@@ -417,11 +401,7 @@ export const SecretMainPage = () => {
|
|||||||
isProtectedBranch={isProtectedBranch}
|
isProtectedBranch={isProtectedBranch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadSecret && <SecretNoAccessListView count={noAccessSecretCount} />}
|
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
|
||||||
{!canReadSecret &&
|
|
||||||
!canReadDynamicSecret &&
|
|
||||||
!canReadSecretImports &&
|
|
||||||
folders?.length === 0 && <PermissionDeniedBanner />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isDetailsLoading && totalCount > 0 && (
|
{!isDetailsLoading && totalCount > 0 && (
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
useSubscription
|
useSubscription
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
@@ -125,6 +125,12 @@ export const ActionBar = ({
|
|||||||
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
||||||
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
||||||
|
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
const shouldCheckFolderPermission = permission.rules.some((rule) =>
|
||||||
|
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
|
||||||
|
);
|
||||||
|
|
||||||
const handleFolderCreate = async (folderName: string) => {
|
const handleFolderCreate = async (folderName: string) => {
|
||||||
try {
|
try {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
@@ -432,12 +438,7 @@ export const ActionBar = ({
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -468,7 +469,12 @@ export const ActionBar = ({
|
|||||||
<div className="flex flex-col space-y-1 p-1.5">
|
<div className="flex flex-col space-y-1 p-1.5">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
|
a={subject(
|
||||||
|
shouldCheckFolderPermission
|
||||||
|
? ProjectPermissionSub.SecretFolders
|
||||||
|
: ProjectPermissionSub.Secrets,
|
||||||
|
{ environment, secretPath }
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -487,13 +493,8 @@ export const ActionBar = ({
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.CreateRootCredential}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -517,10 +518,7 @@ export const ActionBar = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.SecretImports, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -560,12 +558,7 @@ export const ActionBar = ({
|
|||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})}
|
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Move"
|
allowedLabel="Move"
|
||||||
>
|
>
|
||||||
@@ -584,12 +577,7 @@ export const ActionBar = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})}
|
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
|
|||||||
+11
-23
@@ -26,7 +26,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api";
|
import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api";
|
||||||
import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types";
|
import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types";
|
||||||
@@ -60,6 +60,7 @@ export const DynamicSecretLease = ({
|
|||||||
path: secretPath,
|
path: secretPath,
|
||||||
dynamicSecretName
|
dynamicSecretName
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const deleteDynamicSecretLease = useRevokeDynamicSecretLease();
|
const deleteDynamicSecretLease = useRevokeDynamicSecretLease();
|
||||||
|
|
||||||
@@ -139,8 +140,8 @@ export const DynamicSecretLease = ({
|
|||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Renew"
|
allowedLabel="Renew"
|
||||||
>
|
>
|
||||||
@@ -158,8 +159,8 @@ export const DynamicSecretLease = ({
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
@@ -178,11 +179,8 @@ export const DynamicSecretLease = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
{status === DynamicSecretLeaseStatus.FailedDeletion && (
|
{status === DynamicSecretLeaseStatus.FailedDeletion && (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
})}
|
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Force Delete. This action will remove the secret from internal storage, but it will remain in external systems."
|
allowedLabel="Force Delete. This action will remove the secret from internal storage, but it will remain in external systems."
|
||||||
>
|
>
|
||||||
@@ -211,19 +209,9 @@ export const DynamicSecretLease = ({
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
{!isLeaseLoading && Boolean(leases?.length) && (
|
{!isLeaseLoading && Boolean(leases?.length) && (
|
||||||
<div className="mt-6 flex items-center space-x-4">
|
<div className="mt-6 flex items-center space-x-4">
|
||||||
<ProjectPermissionCan
|
<Button onClick={onClickNewLease} size="xs">
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
New Lease
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
</Button>
|
||||||
environment,
|
|
||||||
secretPath
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button onClick={onClickNewLease} size="xs" isDisabled={!isAllowed}>
|
|
||||||
New Lease
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<Button onClick={onClose} variant="plain" colorSchema="secondary" size="xs">
|
<Button onClick={onClose} variant="plain" colorSchema="secondary" size="xs">
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+15
-25
@@ -18,7 +18,7 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteDynamicSecret } from "@app/hooks/api";
|
import { useDeleteDynamicSecret } from "@app/hooks/api";
|
||||||
import {
|
import {
|
||||||
@@ -132,27 +132,17 @@ export const DynamicSecretListView = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 px-4 py-2">
|
<div className="flex items-center space-x-2 px-4 py-2">
|
||||||
<ProjectPermissionCan
|
<Button
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
size="xs"
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
className="m-0 py-0.5 px-2 opacity-0 group-hover:opacity-100"
|
||||||
renderTooltip
|
isDisabled={isRevoking}
|
||||||
allowedLabel="Edit"
|
onClick={(evt) => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
handlePopUpOpen("createDynamicSecretLease", secret);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
Generate
|
||||||
<Button
|
</Button>
|
||||||
size="xs"
|
|
||||||
className="m-0 py-0.5 px-2 opacity-0 group-hover:opacity-100"
|
|
||||||
isDisabled={isRevoking || !isAllowed}
|
|
||||||
onClick={(evt) => {
|
|
||||||
evt.stopPropagation();
|
|
||||||
handlePopUpOpen("createDynamicSecretLease", secret);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Generate
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
|
|
||||||
{secret.status === DynamicSecretStatus.FailedDeletion && (
|
{secret.status === DynamicSecretStatus.FailedDeletion && (
|
||||||
<Tooltip content="This action will remove the secret from internal storage, but it will remain in external systems. Use this option only after you've confirmed that your external leases are handled.">
|
<Tooltip content="This action will remove the secret from internal storage, but it will remain in external systems. Use this option only after you've confirmed that your external leases are handled.">
|
||||||
<Button
|
<Button
|
||||||
@@ -175,8 +165,8 @@ export const DynamicSecretListView = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.EditRootCredential}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Edit"
|
allowedLabel="Edit"
|
||||||
>
|
>
|
||||||
@@ -197,8 +187,8 @@ export const DynamicSecretListView = ({
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.DeleteRootCredential}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { DeleteActionModal, IconButton, Modal, ModalContent } from "@app/components/v2";
|
import { DeleteActionModal, IconButton, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
||||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||||
@@ -33,6 +33,11 @@ export const FolderListView = ({
|
|||||||
"deleteFolder"
|
"deleteFolder"
|
||||||
] as const);
|
] as const);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
const shouldCheckFolderPermission = permission.rules.some((rule) =>
|
||||||
|
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
|
||||||
|
);
|
||||||
|
|
||||||
const { mutateAsync: updateFolder } = useUpdateFolder();
|
const { mutateAsync: updateFolder } = useUpdateFolder();
|
||||||
const { mutateAsync: deleteFolder } = useDeleteFolder();
|
const { mutateAsync: deleteFolder } = useDeleteFolder();
|
||||||
@@ -121,7 +126,12 @@ export const FolderListView = ({
|
|||||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
|
a={subject(
|
||||||
|
shouldCheckFolderPermission
|
||||||
|
? ProjectPermissionSub.SecretFolders
|
||||||
|
: ProjectPermissionSub.Secrets,
|
||||||
|
{ environment, secretPath }
|
||||||
|
)}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Edit"
|
allowedLabel="Edit"
|
||||||
>
|
>
|
||||||
@@ -140,7 +150,12 @@ export const FolderListView = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })}
|
a={subject(
|
||||||
|
shouldCheckFolderPermission
|
||||||
|
? ProjectPermissionSub.SecretFolders
|
||||||
|
: ProjectPermissionSub.Secrets,
|
||||||
|
{ environment, secretPath }
|
||||||
|
)}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
|
|||||||
+1
-6
@@ -142,12 +142,7 @@ export const CopySecretsFromBoard = ({
|
|||||||
<div>
|
<div>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -250,12 +250,7 @@ export const SecretDropzone = ({
|
|||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<input
|
<input
|
||||||
@@ -292,12 +287,7 @@ export const SecretDropzone = ({
|
|||||||
{!isSmaller && (
|
{!isSmaller && (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
+4
-7
@@ -67,7 +67,7 @@ export const SecretImportItem = ({
|
|||||||
isReplicationExpand,
|
isReplicationExpand,
|
||||||
importedSecrets = [],
|
importedSecrets = [],
|
||||||
searchTerm = "",
|
searchTerm = "",
|
||||||
secretPath = "/",
|
secretPath,
|
||||||
environment,
|
environment,
|
||||||
secretImport,
|
secretImport,
|
||||||
onExpandReplicateSecrets: onExpandReplicate
|
onExpandReplicateSecrets: onExpandReplicate
|
||||||
@@ -209,7 +209,7 @@ export const SecretImportItem = ({
|
|||||||
{isReplication && (
|
{isReplication && (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.SecretImports, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Resync replicated secrets"
|
allowedLabel="Resync replicated secrets"
|
||||||
>
|
>
|
||||||
@@ -235,10 +235,7 @@ export const SecretImportItem = ({
|
|||||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.SecretImports, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath: secretPath || "/"
|
|
||||||
})}
|
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Change order"
|
allowedLabel="Change order"
|
||||||
>
|
>
|
||||||
@@ -259,7 +256,7 @@ export const SecretImportItem = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.SecretImports, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
|
|||||||
+15
-61
@@ -84,41 +84,26 @@ export const SecretDetailSidebar = ({
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
values: secret
|
values: secret
|
||||||
});
|
});
|
||||||
|
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
const cannotEditSecret = permission.cannot(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
const isReadOnly =
|
||||||
|
permission.can(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
) && cannotEditSecret;
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: "tags"
|
name: "tags"
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretKey = secret?.key || "";
|
|
||||||
const selectedTags = watch("tags", []) || [];
|
const selectedTags = watch("tags", []) || [];
|
||||||
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
||||||
(prev, curr) => ({ ...prev, [curr.id]: true }),
|
(prev, curr) => ({ ...prev, [curr.id]: true }),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const selectTagSlugs = selectedTags.map((i) => i.slug);
|
|
||||||
|
|
||||||
const cannotEditSecret = permission.cannot(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const isReadOnly =
|
|
||||||
permission.can(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})
|
|
||||||
) && cannotEditSecret;
|
|
||||||
|
|
||||||
const overrideAction = watch("overrideAction");
|
const overrideAction = watch("overrideAction");
|
||||||
const isOverridden =
|
const isOverridden =
|
||||||
@@ -209,12 +194,7 @@ export const SecretDetailSidebar = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -241,12 +221,7 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="mb-2 border-b border-mineshaft-600 pb-4">
|
<div className="mb-2 border-b border-mineshaft-600 pb-4">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Switch
|
<Switch
|
||||||
@@ -302,12 +277,7 @@ export const SecretDetailSidebar = ({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -397,7 +367,6 @@ export const SecretDetailSidebar = ({
|
|||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||||
onClick={() => setCreateReminderFormOpen.on()}
|
onClick={() => setCreateReminderFormOpen.on()}
|
||||||
isDisabled={cannotEditSecret}
|
|
||||||
>
|
>
|
||||||
Create Reminder
|
Create Reminder
|
||||||
</Button>
|
</Button>
|
||||||
@@ -419,12 +388,7 @@ export const SecretDetailSidebar = ({
|
|||||||
render={({ field: { value, onChange, onBlur } }) => (
|
render={({ field: { value, onChange, onBlur } }) => (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Switch
|
<Switch
|
||||||
@@ -486,12 +450,7 @@ export const SecretDetailSidebar = ({
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -506,12 +465,7 @@ export const SecretDetailSidebar = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: selectTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
||||||
|
|||||||
@@ -81,6 +81,15 @@ export const SecretItem = memo(
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
const isReadOnly =
|
||||||
|
permission.can(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
) &&
|
||||||
|
permission.cannot(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -98,8 +107,6 @@ export const SecretItem = memo(
|
|||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretName = watch("key");
|
|
||||||
|
|
||||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||||
const secretReminderNote = watch("reminderNote");
|
const secretReminderNote = watch("reminderNote");
|
||||||
|
|
||||||
@@ -111,33 +118,11 @@ export const SecretItem = memo(
|
|||||||
(prev, curr) => ({ ...prev, [curr.id]: true }),
|
(prev, curr) => ({ ...prev, [curr.id]: true }),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const selectedTagSlugs = selectedTags.map((i) => i.slug);
|
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: "tags"
|
name: "tags"
|
||||||
});
|
});
|
||||||
|
|
||||||
const isReadOnly =
|
|
||||||
permission.can(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: selectedTagSlugs
|
|
||||||
})
|
|
||||||
) &&
|
|
||||||
permission.cannot(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: selectedTagSlugs
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
||||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -324,12 +309,7 @@ export const SecretItem = memo(
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: selectedTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
||||||
@@ -404,12 +384,7 @@ export const SecretItem = memo(
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: selectedTagSlugs
|
|
||||||
})}
|
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Override"
|
allowedLabel="Override"
|
||||||
>
|
>
|
||||||
@@ -465,12 +440,7 @@ export const SecretItem = memo(
|
|||||||
<Popover>
|
<Popover>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: selectedTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||||
@@ -549,12 +519,7 @@ export const SecretItem = memo(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: selectedTagSlugs
|
|
||||||
})}
|
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { WsTag } from "@app/hooks/api/types";
|
|||||||
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
|
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
|
||||||
|
|
||||||
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||||
|
import { Filter } from "../../SecretMainPage.types";
|
||||||
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
|
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
|
||||||
import { SecretItem } from "./SecretItem";
|
import { SecretItem } from "./SecretItem";
|
||||||
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
||||||
@@ -30,6 +31,16 @@ type Props = {
|
|||||||
isProtectedBranch?: boolean;
|
isProtectedBranch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const filterSecrets = (secrets: SecretV3RawSanitized[], filter: Filter) =>
|
||||||
|
secrets.filter(({ key, value, tags }) => {
|
||||||
|
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
|
||||||
|
const searchTerm = filter.searchFilter.toLowerCase();
|
||||||
|
return (
|
||||||
|
(!isTagFilterActive || tags?.some(({ id }) => filter.tags?.[id])) &&
|
||||||
|
(key.toLowerCase().includes(searchTerm) || value?.toLowerCase().includes(searchTerm))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const SecretListView = ({
|
export const SecretListView = ({
|
||||||
secrets = [],
|
secrets = [],
|
||||||
environment,
|
environment,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
faCopy,
|
faCopy,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faKey,
|
faKey,
|
||||||
faLock,
|
|
||||||
faShare,
|
faShare,
|
||||||
faTags
|
faTags
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -72,8 +71,7 @@ export enum FontAwesomeSpriteName {
|
|||||||
Close = "close",
|
Close = "close",
|
||||||
CheckedCircle = "check-circle",
|
CheckedCircle = "check-circle",
|
||||||
ReplicatedSecretKey = "secret-replicated",
|
ReplicatedSecretKey = "secret-replicated",
|
||||||
ShareSecret = "share-secret",
|
ShareSecret = "share-secret"
|
||||||
KeyLock = "key-lock"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is an optimization technique
|
// this is an optimization technique
|
||||||
@@ -90,6 +88,5 @@ export const FontAwesomeSpriteSymbols = [
|
|||||||
{ icon: faClose, symbol: FontAwesomeSpriteName.Close },
|
{ icon: faClose, symbol: FontAwesomeSpriteName.Close },
|
||||||
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle },
|
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle },
|
||||||
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
|
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
|
||||||
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
|
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret }
|
||||||
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock }
|
|
||||||
];
|
];
|
||||||
|
|||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
import { FontAwesomeSymbol, Input, Tooltip } from "@app/components/v2";
|
|
||||||
|
|
||||||
import { FontAwesomeSpriteName } from "./SecretListView.utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SecretNoAccessListView = ({ count }: Props) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{Array.from(Array(count)).map((_, i) => (
|
|
||||||
<Tooltip
|
|
||||||
className="max-w-sm"
|
|
||||||
asChild
|
|
||||||
content="You do not have permission to view this secret"
|
|
||||||
key={`no-access-secret-${i + 1}`}
|
|
||||||
>
|
|
||||||
<div className="flex border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700">
|
|
||||||
<div className="flex h-11 w-11 items-center justify-center px-4 py-3">
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
className="ml-3 block h-3.5 w-3.5"
|
|
||||||
symbolName={FontAwesomeSpriteName.KeyLock}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
|
||||||
<Input
|
|
||||||
autoComplete="off"
|
|
||||||
isReadOnly
|
|
||||||
variant="plain"
|
|
||||||
value="NO ACCESS"
|
|
||||||
isDisabled
|
|
||||||
className="w-full px-0 blur-sm placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
|
|
||||||
tabIndex={0}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
<span className="blur">********</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export { SecretListView } from "./SecretListView";
|
export { SecretListView } from "./SecretListView";
|
||||||
export { SecretNoAccessListView } from "./SecretNoAccessListView";
|
|
||||||
|
|||||||
@@ -71,10 +71,7 @@ import { SecretType, TSecretFolder } from "@app/hooks/api/types";
|
|||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
|
import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils";
|
||||||
import { SecretOverviewDynamicSecretRow } from "@app/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow";
|
import { SecretOverviewDynamicSecretRow } from "@app/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow";
|
||||||
import {
|
import { SecretOverviewTableRow } from "@app/views/SecretOverviewPage/components/SecretOverviewTableRow";
|
||||||
SecretNoAccessOverviewTableRow,
|
|
||||||
SecretOverviewTableRow
|
|
||||||
} from "@app/views/SecretOverviewPage/components/SecretOverviewTableRow";
|
|
||||||
import { SecretTableResourceCount } from "@app/views/SecretOverviewPage/components/SecretTableResourceCount";
|
import { SecretTableResourceCount } from "@app/views/SecretOverviewPage/components/SecretTableResourceCount";
|
||||||
|
|
||||||
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
||||||
@@ -242,10 +239,7 @@ export const SecretOverviewPage = () => {
|
|||||||
totalFolderCount,
|
totalFolderCount,
|
||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalDynamicSecretCount,
|
totalDynamicSecretCount,
|
||||||
totalCount = 0,
|
totalCount = 0
|
||||||
totalUniqueFoldersInPage,
|
|
||||||
totalUniqueSecretsInPage,
|
|
||||||
totalUniqueDynamicSecretsInPage
|
|
||||||
} = overview ?? {};
|
} = overview ?? {};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -309,7 +303,7 @@ export const SecretOverviewPage = () => {
|
|||||||
if (
|
if (
|
||||||
permission.can(
|
permission.can(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
subject(ProjectPermissionSub.SecretFolders, { environment: env.slug, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath })
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const folder = getFolderByNameAndEnv(oldFolderName, env.slug);
|
const folder = getFolderByNameAndEnv(oldFolderName, env.slug);
|
||||||
@@ -512,13 +506,20 @@ export const SecretOverviewPage = () => {
|
|||||||
const pathSegment = secretPath.split("/").filter(Boolean);
|
const pathSegment = secretPath.split("/").filter(Boolean);
|
||||||
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
|
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
|
||||||
const folderName = pathSegment.at(-1);
|
const folderName = pathSegment.at(-1);
|
||||||
const canCreateFolder = permission.can(
|
const canCreateFolder = permission.rules.some((rule) =>
|
||||||
ProjectPermissionActions.Create,
|
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
|
||||||
subject(ProjectPermissionSub.SecretFolders, {
|
)
|
||||||
environment: slug,
|
? permission.can(
|
||||||
secretPath: parentPath
|
ProjectPermissionActions.Create,
|
||||||
})
|
subject(ProjectPermissionSub.SecretFolders, {
|
||||||
);
|
environment: slug,
|
||||||
|
secretPath: parentPath
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: permission.can(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: slug, secretPath: parentPath })
|
||||||
|
);
|
||||||
if (folderName && parentPath && canCreateFolder) {
|
if (folderName && parentPath && canCreateFolder) {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
@@ -770,7 +771,7 @@ export const SecretOverviewPage = () => {
|
|||||||
<div className="flex flex-col space-y-1 p-1.5">
|
<div className="flex flex-col space-y-1 p-1.5">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.SecretFolders}
|
a={subject(ProjectPermissionSub.Secrets, { secretPath })}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -974,16 +975,6 @@ export const SecretOverviewPage = () => {
|
|||||||
expandableColWidth={expandableTableWidth}
|
expandableColWidth={expandableTableWidth}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<SecretNoAccessOverviewTableRow
|
|
||||||
environments={visibleEnvs}
|
|
||||||
count={Math.max(
|
|
||||||
(page * perPage > totalCount ? totalCount % perPage : perPage) -
|
|
||||||
(totalUniqueFoldersInPage || 0) -
|
|
||||||
(totalUniqueDynamicSecretsInPage || 0) -
|
|
||||||
(totalUniqueSecretsInPage || 0),
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TBody>
|
</TBody>
|
||||||
|
|||||||
+18
-11
@@ -93,14 +93,23 @@ export const CreateSecretForm = ({
|
|||||||
const pathSegment = secretPath.split("/").filter(Boolean);
|
const pathSegment = secretPath.split("/").filter(Boolean);
|
||||||
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
|
const parentPath = `/${pathSegment.slice(0, -1).join("/")}`;
|
||||||
const folderName = pathSegment.at(-1);
|
const folderName = pathSegment.at(-1);
|
||||||
const canCreateFolder = permission.can(
|
const canCreateFolder = permission.rules.some((rule) =>
|
||||||
ProjectPermissionActions.Create,
|
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
|
||||||
subject(ProjectPermissionSub.SecretFolders, {
|
)
|
||||||
environment: env.slug,
|
? permission.can(
|
||||||
secretPath: parentPath
|
ProjectPermissionActions.Create,
|
||||||
})
|
subject(ProjectPermissionSub.SecretFolders, {
|
||||||
);
|
environment: env.slug,
|
||||||
|
secretPath: parentPath
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: permission.can(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment: env.slug,
|
||||||
|
secretPath: parentPath
|
||||||
|
})
|
||||||
|
);
|
||||||
if (folderName && parentPath && canCreateFolder) {
|
if (folderName && parentPath && canCreateFolder) {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
@@ -241,9 +250,7 @@ export const CreateSecretForm = ({
|
|||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Create,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
environment: environmentSlug.slug,
|
environment: environmentSlug.slug,
|
||||||
secretPath,
|
secretPath
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+8
-17
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback,useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { subject } from "@casl/ability";
|
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";
|
||||||
@@ -7,7 +7,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
|
import { DeleteActionModal,IconButton, Tooltip } from "@app/components/v2";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
@@ -63,8 +63,8 @@ export const SecretEditRow = ({
|
|||||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const toggleModal = useCallback(() => {
|
const toggleModal = useCallback(() => {
|
||||||
setIsModalOpen((prev) => !prev);
|
setIsModalOpen((prev) => !prev)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const handleFormReset = () => {
|
const handleFormReset = () => {
|
||||||
reset();
|
reset();
|
||||||
@@ -114,6 +114,7 @@ export const SecretEditRow = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex w-full cursor-text items-center space-x-2">
|
<div className="group flex w-full cursor-text items-center space-x-2">
|
||||||
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={toggleModal}
|
onClose={toggleModal}
|
||||||
@@ -150,13 +151,8 @@ export const SecretEditRow = ({
|
|||||||
{isDirty ? (
|
{isDirty ? (
|
||||||
<>
|
<>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={isCreatable ? ProjectPermissionActions.Create : ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: ["*"]
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div>
|
<div>
|
||||||
@@ -205,12 +201,7 @@ export const SecretEditRow = ({
|
|||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={ProjectPermissionSub.Secrets}
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: ["*"]
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
|||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
import { faCircle } from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { Td, Tooltip, Tr } from "@app/components/v2";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
environments: { name: string; slug: string }[];
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SecretNoAccessOverviewTableRow = ({ environments = [], count }: Props) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{Array.from(Array(count)).map((_, j) => (
|
|
||||||
<Tr key={`no-access-secret-overview-${j + 1}`} isHoverable isSelectable className="group">
|
|
||||||
<Td className="sticky left-0 z-10 bg-mineshaft-800 bg-clip-padding py-0 px-0 group-hover:bg-mineshaft-700">
|
|
||||||
<div className="h-full w-full border-r border-mineshaft-600 py-2.5 px-5">
|
|
||||||
<Tooltip
|
|
||||||
asChild
|
|
||||||
content="You do not have permission to view this secret"
|
|
||||||
className="max-w-sm"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-5">
|
|
||||||
<div className="text-bunker-300">
|
|
||||||
<FontAwesomeIcon className="block" icon={faLock} />
|
|
||||||
</div>
|
|
||||||
<div className="blur-sm">NO ACCESS</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
{environments.map(({ slug }, i) => {
|
|
||||||
return (
|
|
||||||
<Td
|
|
||||||
key={`sec-overview-${slug}-${i + 1}-value`}
|
|
||||||
className="py-0 px-0 group-hover:bg-mineshaft-700"
|
|
||||||
>
|
|
||||||
<div className="h-full w-full border-r border-mineshaft-600 py-[0.85rem] px-5">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<FontAwesomeIcon icon={faCircle} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+9
-10
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { useUpdateSecretV3 } from "@app/hooks/api";
|
import { useUpdateSecretV3 } from "@app/hooks/api";
|
||||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||||
import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils";
|
import { SecretActionType } from "@app/views/SecretMainPage/components/SecretListView/SecretListView.utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -42,16 +42,15 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
|
|||||||
|
|
||||||
const isReadOnly = environments.some((env) => {
|
const isReadOnly = environments.some((env) => {
|
||||||
const environment = env.slug;
|
const environment = env.slug;
|
||||||
const secretDetails = getSecretByKey(environment, secretKey);
|
|
||||||
const secretPermissionSubject = subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName: secretKey,
|
|
||||||
secretTags: (secretDetails?.tags || []).map((i) => i.slug)
|
|
||||||
});
|
|
||||||
const isSecretInEnvReadOnly =
|
const isSecretInEnvReadOnly =
|
||||||
permission.can(ProjectPermissionActions.Read, secretPermissionSubject) &&
|
permission.can(
|
||||||
permission.cannot(ProjectPermissionActions.Edit, secretPermissionSubject);
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
) &&
|
||||||
|
permission.cannot(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
if (isSecretInEnvReadOnly) {
|
if (isSecretInEnvReadOnly) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export { SecretNoAccessOverviewTableRow } from "./SecretNoAccessOverviewTableRow";
|
|
||||||
export { SecretOverviewTableRow } from "./SecretOverviewTableRow";
|
export { SecretOverviewTableRow } from "./SecretOverviewTableRow";
|
||||||
|
|||||||
+24
-38
@@ -49,9 +49,9 @@ export const SelectionPanel = ({
|
|||||||
"bulkDeleteEntries"
|
"bulkDeleteEntries"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const selectedFolderCount = Object.keys(selectedEntries.folder).length;
|
const selectedFolderCount = Object.keys(selectedEntries.folder).length
|
||||||
const selectedKeysCount = Object.keys(selectedEntries.secret).length;
|
const selectedKeysCount = Object.keys(selectedEntries.secret).length
|
||||||
const selectedCount = selectedFolderCount + selectedKeysCount;
|
const selectedCount = selectedFolderCount + selectedKeysCount
|
||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
@@ -65,12 +65,7 @@ export const SelectionPanel = ({
|
|||||||
const shouldShowDelete = userAvailableEnvs.some((env) =>
|
const shouldShowDelete = userAvailableEnvs.some((env) =>
|
||||||
permission.can(
|
permission.can(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionActions.Delete,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath })
|
||||||
environment: env.slug,
|
|
||||||
secretPath,
|
|
||||||
secretName: "*",
|
|
||||||
secretTags: ["*"]
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -82,50 +77,41 @@ export const SelectionPanel = ({
|
|||||||
return "Do you want to delete the selected secrets across environments?";
|
return "Do you want to delete the selected secrets across environments?";
|
||||||
}
|
}
|
||||||
return "Do you want to delete the selected folders across environments?";
|
return "Do you want to delete the selected folders across environments?";
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleBulkDelete = async () => {
|
const handleBulkDelete = async () => {
|
||||||
let processedEntries = 0;
|
let processedEntries = 0;
|
||||||
|
|
||||||
const promises = userAvailableEnvs.map(async (env) => {
|
const promises = userAvailableEnvs.map(async (env) => {
|
||||||
// additional check: ensure that bulk delete is only executed on envs that user has access to
|
// additional check: ensure that bulk delete is only executed on envs that user has access to
|
||||||
|
|
||||||
if (
|
if (
|
||||||
permission.can(
|
permission.cannot(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionActions.Delete,
|
||||||
subject(ProjectPermissionSub.SecretFolders, { environment: env.slug, secretPath })
|
subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath })
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await Promise.all(
|
return;
|
||||||
Object.keys(selectedEntries.folder).map(async (folderName) => {
|
|
||||||
const folder = getFolderByNameAndEnv(folderName, env.slug);
|
|
||||||
if (folder) {
|
|
||||||
processedEntries += 1;
|
|
||||||
await deleteFolder({
|
|
||||||
folderId: folder?.id,
|
|
||||||
path: secretPath,
|
|
||||||
environment: env.slug,
|
|
||||||
projectId: workspaceId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Object.keys(selectedEntries.folder).map(async (folderName) => {
|
||||||
|
const folder = getFolderByNameAndEnv(folderName, env.slug);
|
||||||
|
if (folder) {
|
||||||
|
processedEntries += 1;
|
||||||
|
await deleteFolder({
|
||||||
|
folderId: folder?.id,
|
||||||
|
path: secretPath,
|
||||||
|
environment: env.slug,
|
||||||
|
projectId: workspaceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const secretsToDelete = Object.keys(selectedEntries.secret).reduce(
|
const secretsToDelete = Object.keys(selectedEntries.secret).reduce(
|
||||||
(accum: TDeleteSecretBatchDTO["secrets"], secretName) => {
|
(accum: TDeleteSecretBatchDTO["secrets"], secretName) => {
|
||||||
const entry = getSecretByKey(env.slug, secretName);
|
const entry = getSecretByKey(env.slug, secretName);
|
||||||
const canDeleteSecret = permission.can(
|
if (entry) {
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: env.slug,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: (entry?.tags || []).map((i) => i.slug)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (entry && canDeleteSecret) {
|
|
||||||
return [
|
return [
|
||||||
...accum,
|
...accum,
|
||||||
{
|
{
|
||||||
|
|||||||
+4
-1
@@ -136,7 +136,10 @@ export const DeleteProjectSection = () => {
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
|
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p>
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Delete} a={ProjectPermissionSub.Project}>
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Delete}
|
||||||
|
a={ProjectPermissionSub.Workspace}
|
||||||
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
isLoading={isDeleting}
|
isLoading={isDeleting}
|
||||||
|
|||||||
+4
-1
@@ -318,7 +318,10 @@ export const EncryptionTab = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Workspace}
|
||||||
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
|
|||||||
+3
-2
@@ -22,6 +22,7 @@ const formSchema = yup.object({
|
|||||||
type FormData = yup.InferType<typeof formSchema>;
|
type FormData = yup.InferType<typeof formSchema>;
|
||||||
|
|
||||||
export const ProjectNameChangeSection = () => {
|
export const ProjectNameChangeSection = () => {
|
||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { mutateAsync, isLoading } = useRenameWorkspace();
|
const { mutateAsync, isLoading } = useRenameWorkspace();
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ export const ProjectNameChangeSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-md">
|
<div className="max-w-md">
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Controller
|
<Controller
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
@@ -102,7 +103,7 @@ export const ProjectNameChangeSection = () => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Workspace}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
|
|||||||
Reference in New Issue
Block a user