mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 21:26:04 +00:00
Merge pull request #2485 from akhilmhdh/feat/permission-ui
New project permission ui
This commit is contained in:
Vendored
+8
@@ -101,6 +101,9 @@ import {
|
|||||||
TIdentityKubernetesAuths,
|
TIdentityKubernetesAuths,
|
||||||
TIdentityKubernetesAuthsInsert,
|
TIdentityKubernetesAuthsInsert,
|
||||||
TIdentityKubernetesAuthsUpdate,
|
TIdentityKubernetesAuthsUpdate,
|
||||||
|
TIdentityMetadata,
|
||||||
|
TIdentityMetadataInsert,
|
||||||
|
TIdentityMetadataUpdate,
|
||||||
TIdentityOidcAuths,
|
TIdentityOidcAuths,
|
||||||
TIdentityOidcAuthsInsert,
|
TIdentityOidcAuthsInsert,
|
||||||
TIdentityOidcAuthsUpdate,
|
TIdentityOidcAuthsUpdate,
|
||||||
@@ -546,6 +549,11 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityUniversalAuthsInsert,
|
TIdentityUniversalAuthsInsert,
|
||||||
TIdentityUniversalAuthsUpdate
|
TIdentityUniversalAuthsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.IdentityMetadata]: KnexOriginal.CompositeTableType<
|
||||||
|
TIdentityMetadata,
|
||||||
|
TIdentityMetadataInsert,
|
||||||
|
TIdentityMetadataUpdate
|
||||||
|
>;
|
||||||
[TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType<
|
[TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType<
|
||||||
TIdentityKubernetesAuths,
|
TIdentityKubernetesAuths,
|
||||||
TIdentityKubernetesAuthsInsert,
|
TIdentityKubernetesAuthsInsert,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.IdentityMetadata))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityMetadata, (tb) => {
|
||||||
|
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
tb.string("key").notNullable();
|
||||||
|
tb.string("value").notNullable();
|
||||||
|
tb.uuid("orgId").notNullable();
|
||||||
|
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
tb.uuid("userId");
|
||||||
|
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
tb.uuid("identityId");
|
||||||
|
tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||||
|
tb.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityMetadata);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const IdentityMetadataSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
userId: z.string().uuid().nullable().optional(),
|
||||||
|
identityId: z.string().uuid().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityMetadata = z.infer<typeof IdentityMetadataSchema>;
|
||||||
|
export type TIdentityMetadataInsert = Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>;
|
||||||
|
export type TIdentityMetadataUpdate = Partial<Omit<z.input<typeof IdentityMetadataSchema>, TImmutableDBKeys>>;
|
||||||
@@ -31,6 +31,7 @@ export * from "./identity-aws-auths";
|
|||||||
export * from "./identity-azure-auths";
|
export * from "./identity-azure-auths";
|
||||||
export * from "./identity-gcp-auths";
|
export * from "./identity-gcp-auths";
|
||||||
export * from "./identity-kubernetes-auths";
|
export * from "./identity-kubernetes-auths";
|
||||||
|
export * from "./identity-metadata";
|
||||||
export * from "./identity-oidc-auths";
|
export * from "./identity-oidc-auths";
|
||||||
export * from "./identity-org-memberships";
|
export * from "./identity-org-memberships";
|
||||||
export * from "./identity-project-additional-privilege";
|
export * from "./identity-project-additional-privilege";
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export enum TableName {
|
|||||||
IdentityProjectMembership = "identity_project_memberships",
|
IdentityProjectMembership = "identity_project_memberships",
|
||||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||||
|
// used by both identity and users
|
||||||
|
IdentityMetadata = "identity_metadata",
|
||||||
ScimToken = "scim_tokens",
|
ScimToken = "scim_tokens",
|
||||||
AccessApprovalPolicy = "access_approval_policies",
|
AccessApprovalPolicy = "access_approval_policies",
|
||||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ 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 { ProjectPermissionSchema } 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";
|
||||||
import { ProjectPermissionSchema, SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
async (req, profile, cb) => {
|
async (req, profile, cb) => {
|
||||||
try {
|
try {
|
||||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||||
|
|
||||||
const email =
|
const email =
|
||||||
profile?.email ??
|
profile?.email ??
|
||||||
// entra sends data in this format
|
// entra sends data in this format
|
||||||
@@ -123,6 +124,14 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userMetadata = Object.keys(profile.attributes || {})
|
||||||
|
.map((key) => {
|
||||||
|
// for the ones like in format: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email
|
||||||
|
const formatedKey = key.startsWith("http") ? key.split("/").at(-1) || "" : key;
|
||||||
|
return { key: formatedKey, value: String((profile.attributes as Record<string, string>)[key]) };
|
||||||
|
})
|
||||||
|
.filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key));
|
||||||
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||||
externalId: profile.nameID,
|
externalId: profile.nameID,
|
||||||
email,
|
email,
|
||||||
@@ -130,7 +139,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
lastName: lastName as string,
|
lastName: lastName as string,
|
||||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||||
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string,
|
||||||
|
metadata: userMetadata
|
||||||
});
|
});
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+6
-12
@@ -34,18 +34,12 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
|||||||
|
|
||||||
// TODO(akhilmhdh): move this to more centralized
|
// TODO(akhilmhdh): move this to more centralized
|
||||||
export const UnpackedPermissionSchema = z.object({
|
export const UnpackedPermissionSchema = z.object({
|
||||||
subject: z.union([z.string().min(1), z.string().array()]).optional(),
|
subject: z
|
||||||
action: z.union([z.string().min(1), z.string().array()]),
|
.union([z.string().min(1), z.string().array()])
|
||||||
conditions: z
|
.transform((el) => (typeof el !== "string" ? el[0] : el))
|
||||||
.object({
|
.optional(),
|
||||||
environment: z.string().optional(),
|
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
|
||||||
secretPath: z
|
conditions: z.unknown().optional()
|
||||||
.object({
|
|
||||||
$glob: z.string().min(1)
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const unpackPermissions = (permissions: unknown) =>
|
const unpackPermissions = (permissions: unknown) =>
|
||||||
|
|||||||
@@ -168,8 +168,14 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
})
|
})
|
||||||
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
|
||||||
|
.andOn(`${TableName.Organization}.id`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("username").withSchema(TableName.Users).as("username"),
|
||||||
// groups specific
|
// groups specific
|
||||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||||
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||||
@@ -257,6 +263,9 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||||
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
||||||
// general
|
// general
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||||
@@ -267,6 +276,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
key: "projectId",
|
key: "projectId",
|
||||||
parentMapper: ({
|
parentMapper: ({
|
||||||
orgId,
|
orgId,
|
||||||
|
username,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
membershipId,
|
membershipId,
|
||||||
groupMembershipId,
|
groupMembershipId,
|
||||||
@@ -279,6 +289,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
|
username,
|
||||||
id: membershipId || groupMembershipId,
|
id: membershipId || groupMembershipId,
|
||||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||||
@@ -354,6 +365,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||||
isTemporary: userAdditionalPrivilegesIsTemporary
|
isTemporary: userAdditionalPrivilegesIsTemporary
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -399,6 +419,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
|
||||||
`${TableName.IdentityProjectMembership}.id`
|
`${TableName.IdentityProjectMembership}.id`
|
||||||
)
|
)
|
||||||
|
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ProjectRoles,
|
TableName.ProjectRoles,
|
||||||
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||||
@@ -415,11 +436,17 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.IdentityProjectMembership}.projectId`,
|
`${TableName.IdentityProjectMembership}.projectId`,
|
||||||
`${TableName.Project}.id`
|
`${TableName.Project}.id`
|
||||||
)
|
)
|
||||||
.where("identityId", identityId)
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
|
||||||
|
.andOn(`${TableName.Project}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
|
.where(`${TableName.IdentityProjectMembership}.identityId`, identityId)
|
||||||
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
db.ref("id").withSchema(TableName.IdentityProjectMembership).as("membershipId"),
|
||||||
|
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||||
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
db.ref("orgId").withSchema(TableName.Project).as("orgId"), // Now you can select orgId from Project
|
||||||
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||||
@@ -443,15 +470,19 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
db
|
db
|
||||||
.ref("temporaryAccessEndTime")
|
.ref("temporaryAccessEndTime")
|
||||||
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
.withSchema(TableName.IdentityProjectAdditionalPrivilege)
|
||||||
.as("identityApTemporaryAccessEndTime")
|
.as("identityApTemporaryAccessEndTime"),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
);
|
);
|
||||||
|
|
||||||
const permission = sqlNestRelationships({
|
const permission = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "membershipId",
|
key: "membershipId",
|
||||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
|
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
|
||||||
id: membershipId,
|
id: membershipId,
|
||||||
identityId,
|
identityId,
|
||||||
|
username: identityName,
|
||||||
projectId,
|
projectId,
|
||||||
createdAt: membershipCreatedAt,
|
createdAt: membershipCreatedAt,
|
||||||
updatedAt: membershipUpdatedAt,
|
updatedAt: membershipUpdatedAt,
|
||||||
@@ -489,6 +520,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
||||||
isTemporary: identityApIsTemporary
|
isTemporary: identityApIsTemporary
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export type TBuildProjectPermissionDTO = {
|
||||||
|
permissions?: unknown;
|
||||||
|
role: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export type TBuildOrgPermissionDTO = {
|
||||||
|
permissions?: unknown;
|
||||||
|
role: string;
|
||||||
|
}[];
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
import { MongoQuery } from "@ucast/mongo2js";
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
|
import handlebars from "handlebars";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OrgMembershipRole,
|
OrgMembershipRole,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { conditionsMatcher } from "@app/lib/casl";
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { objectify } from "@app/lib/fn";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -20,7 +22,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
|||||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||||
import { TPermissionDALFactory } from "./permission-dal";
|
import { TPermissionDALFactory } from "./permission-dal";
|
||||||
import { validateOrgSAML } from "./permission-fns";
|
import { validateOrgSAML } from "./permission-fns";
|
||||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types";
|
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
|
||||||
import {
|
import {
|
||||||
buildServiceTokenProjectPermission,
|
buildServiceTokenProjectPermission,
|
||||||
projectAdminPermissions,
|
projectAdminPermissions,
|
||||||
@@ -72,7 +74,7 @@ export const permissionServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||||
const rules = projectUserRoles
|
const rules = projectUserRoles
|
||||||
.map(({ role, permissions }) => {
|
.map(({ role, permissions }) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
@@ -98,9 +100,7 @@ export const permissionServiceFactory = ({
|
|||||||
})
|
})
|
||||||
.reduce((curr, prev) => prev.concat(curr), []);
|
.reduce((curr, prev) => prev.concat(curr), []);
|
||||||
|
|
||||||
return createMongoAbility<ProjectPermissionSet>(rules, {
|
return rules;
|
||||||
conditionsMatcher
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -223,8 +223,32 @@ export const permissionServiceFactory = ({
|
|||||||
permissions
|
permissions
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
|
||||||
|
const metadataKeyValuePair = objectify(
|
||||||
|
userProjectPermission.metadata,
|
||||||
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
);
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
id: userProjectPermission.userId,
|
||||||
|
username: userProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
permission,
|
||||||
membership: userProjectPermission,
|
membership: userProjectPermission,
|
||||||
hasRole: (role: string) =>
|
hasRole: (role: string) =>
|
||||||
userProjectPermission.roles.findIndex(
|
userProjectPermission.roles.findIndex(
|
||||||
@@ -262,8 +286,32 @@ export const permissionServiceFactory = ({
|
|||||||
permissions
|
permissions
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
|
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||||
|
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false, strict: true });
|
||||||
|
const metadataKeyValuePair = objectify(
|
||||||
|
identityProjectPermission.metadata,
|
||||||
|
(i) => i.key,
|
||||||
|
(i) => i.value
|
||||||
|
);
|
||||||
|
const interpolateRules = templatedRules(
|
||||||
|
{
|
||||||
|
identity: {
|
||||||
|
id: identityProjectPermission.identityId,
|
||||||
|
username: identityProjectPermission.username,
|
||||||
|
metadata: metadataKeyValuePair
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ data: false }
|
||||||
|
);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(
|
||||||
|
JSON.parse(interpolateRules) as RawRuleOf<MongoAbility<ProjectPermissionSet>>[],
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
permission,
|
||||||
membership: identityProjectPermission,
|
membership: identityProjectPermission,
|
||||||
hasRole: (role: string) =>
|
hasRole: (role: string) =>
|
||||||
identityProjectPermission.roles.findIndex(
|
identityProjectPermission.roles.findIndex(
|
||||||
@@ -346,14 +394,22 @@ export const permissionServiceFactory = ({
|
|||||||
if (isCustomRole) {
|
if (isCustomRole) {
|
||||||
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||||
if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
|
if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
|
||||||
|
const rules = buildProjectPermissionRules([
|
||||||
|
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
permission: buildProjectPermission([
|
permission: createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
conditionsMatcher
|
||||||
]),
|
}),
|
||||||
role: projectRole
|
role: projectRole
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
|
|
||||||
|
const rules = buildProjectPermissionRules([{ role, permissions: [] }]);
|
||||||
|
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
|
return { permission };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -364,6 +420,6 @@ export const permissionServiceFactory = ({
|
|||||||
getOrgPermissionByRole,
|
getOrgPermissionByRole,
|
||||||
getProjectPermissionByRole,
|
getProjectPermissionByRole,
|
||||||
buildOrgPermission,
|
buildOrgPermission,
|
||||||
buildProjectPermission
|
buildProjectPermissionRules
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,47 @@
|
|||||||
export type TBuildProjectPermissionDTO = {
|
import picomatch from "picomatch";
|
||||||
permissions?: unknown;
|
import { z } from "zod";
|
||||||
role: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
export type TBuildOrgPermissionDTO = {
|
export enum PermissionConditionOperators {
|
||||||
permissions?: unknown;
|
$IN = "$in",
|
||||||
role: string;
|
$ALL = "$all",
|
||||||
}[];
|
$REGEX = "$regex",
|
||||||
|
$EQ = "$eq",
|
||||||
|
$NEQ = "$ne",
|
||||||
|
$GLOB = "$glob"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionConditionSchema = {
|
||||||
|
[PermissionConditionOperators.$IN]: z.string().min(1).array(),
|
||||||
|
[PermissionConditionOperators.$ALL]: z.string().min(1).array(),
|
||||||
|
[PermissionConditionOperators.$REGEX]: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine(
|
||||||
|
(el) => {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new RegExp(el);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ message: "Invalid regex pattern" }
|
||||||
|
),
|
||||||
|
[PermissionConditionOperators.$EQ]: z.string().min(1),
|
||||||
|
[PermissionConditionOperators.$NEQ]: z.string().min(1),
|
||||||
|
[PermissionConditionOperators.$GLOB]: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine(
|
||||||
|
(el) => {
|
||||||
|
try {
|
||||||
|
picomatch.parse([el]);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ message: "Invalid glob pattern" }
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
import { conditionsMatcher } from "@app/lib/casl";
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
||||||
|
|
||||||
export enum ProjectPermissionActions {
|
export enum ProjectPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@@ -37,7 +41,25 @@ export enum ProjectPermissionSub {
|
|||||||
Kms = "kms"
|
Kms = "kms"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
export type SecretSubjectFields = {
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
// secretName: 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 = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
};
|
};
|
||||||
@@ -45,11 +67,14 @@ type SubjectFields = {
|
|||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
||||||
]
|
]
|
||||||
| [
|
| [
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
(
|
||||||
|
| ProjectPermissionSub.SecretFolders
|
||||||
|
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
|
||||||
|
)
|
||||||
]
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||||
@@ -76,128 +101,230 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||||
|
|
||||||
export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermissionSub][] = [
|
const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Secrets],
|
z
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Secrets],
|
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets],
|
.transform((el) => (typeof el === "string" ? [el] : el));
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets],
|
|
||||||
|
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval],
|
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval],
|
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval],
|
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval],
|
|
||||||
|
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation],
|
const SecretConditionSchema = z
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation],
|
.object({
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation],
|
environment: z.union([
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation],
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.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()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback],
|
export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback],
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Member],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Member],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Member],
|
),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Member],
|
conditions: SecretConditionSchema.describe(
|
||||||
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Groups],
|
).optional()
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Groups],
|
}),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Groups],
|
z.object({
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Groups],
|
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Role],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Role],
|
)
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Role],
|
}),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Role],
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Integrations],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Integrations],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations],
|
)
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations],
|
}),
|
||||||
|
z.object({
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks],
|
subject: z.literal(ProjectPermissionSub.SecretRollback).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks],
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe(
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks],
|
)
|
||||||
|
}),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Identity],
|
z.object({
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Identity],
|
subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Identity],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Identity],
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens],
|
}),
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens],
|
z.object({
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens],
|
subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Settings],
|
)
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Settings],
|
}),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Settings],
|
z.object({
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Settings],
|
subject: z.literal(ProjectPermissionSub.Role).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Environments],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Environments],
|
)
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Environments],
|
}),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Environments],
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Integrations).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Tags],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Tags],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
|
)
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
|
}),
|
||||||
|
z.object({
|
||||||
// TODO(Daniel): Remove the audit logs permissions from project-level permissions.
|
subject: z.literal(ProjectPermissionSub.Webhooks).describe("The entity this permission pertains to."),
|
||||||
// TODO: We haven't done this yet because it might break existing roles, since those roles will become "invalid" since the audit log permission defined on those roles, no longer exist in the project-level defined permissions.
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
|
)
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
|
}),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs],
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList],
|
)
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList],
|
}),
|
||||||
|
z.object({
|
||||||
// double check if all CRUD are needed for CA and Certificates
|
subject: z.literal(ProjectPermissionSub.ServiceTokens).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities],
|
)
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities],
|
}),
|
||||||
|
z.object({
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Certificates],
|
subject: z.literal(ProjectPermissionSub.Settings).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Certificates],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates],
|
)
|
||||||
|
}),
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates],
|
z.object({
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates],
|
subject: z.literal(ProjectPermissionSub.Environments).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates],
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts],
|
}),
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts],
|
z.object({
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts],
|
subject: z.literal(ProjectPermissionSub.Tags).describe("The entity this permission pertains to."),
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts],
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections],
|
)
|
||||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections],
|
}),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections],
|
z.object({
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections],
|
subject: z.literal(ProjectPermissionSub.AuditLogs).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Project],
|
"Describe what action an entity can take."
|
||||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Project],
|
)
|
||||||
|
}),
|
||||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
z.object({
|
||||||
];
|
subject: z.literal(ProjectPermissionSub.IpAllowList).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.CertificateAuthorities).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Certificates).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.CertificateTemplates).describe("The entity this permission pertains to. "),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.PkiCollections).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Project).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete]).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.Kms).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Edit]).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SecretFolders).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read]).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
const buildAdminPermissionRules = () => {
|
const buildAdminPermissionRules = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
// Admins get full access to everything
|
// Admins get full access to everything
|
||||||
fullProjectPermissionSet.forEach((permission) => {
|
[
|
||||||
const [action, subject] = permission;
|
ProjectPermissionSub.Secrets,
|
||||||
can(action, subject);
|
ProjectPermissionSub.SecretApproval,
|
||||||
|
ProjectPermissionSub.SecretRotation,
|
||||||
|
ProjectPermissionSub.Member,
|
||||||
|
ProjectPermissionSub.Groups,
|
||||||
|
ProjectPermissionSub.Role,
|
||||||
|
ProjectPermissionSub.Integrations,
|
||||||
|
ProjectPermissionSub.Webhooks,
|
||||||
|
ProjectPermissionSub.Identity,
|
||||||
|
ProjectPermissionSub.ServiceTokens,
|
||||||
|
ProjectPermissionSub.Settings,
|
||||||
|
ProjectPermissionSub.Environments,
|
||||||
|
ProjectPermissionSub.Tags,
|
||||||
|
ProjectPermissionSub.AuditLogs,
|
||||||
|
ProjectPermissionSub.IpAllowList,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities,
|
||||||
|
ProjectPermissionSub.Certificates,
|
||||||
|
ProjectPermissionSub.CertificateTemplates,
|
||||||
|
ProjectPermissionSub.PkiAlerts,
|
||||||
|
ProjectPermissionSub.PkiCollections
|
||||||
|
].forEach((el) => {
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
el as ProjectPermissionSub
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
|
||||||
|
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||||
|
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,73 +333,116 @@ export const projectAdminPermissions = buildAdminPermissionRules();
|
|||||||
const buildMemberPermissionRules = () => {
|
const buildMemberPermissionRules = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Integrations
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Webhooks
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.ServiceTokens
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Settings
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Environments
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Tags
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
// double check if all CRUD are needed for CA and Certificates
|
// double check if all CRUD are needed for CA and Certificates
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
can(
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
[
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
ProjectPermissionActions.Read,
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.Certificates
|
||||||
|
);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
@@ -382,32 +552,19 @@ export const isAtLeastAsPrivilegedWorkspace = (
|
|||||||
|
|
||||||
return set1.size >= set2.size;
|
return set1.size >= set2.size;
|
||||||
};
|
};
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
/*
|
export const SecretV2SubjectFieldMapper = (arg: string) => {
|
||||||
* Case: The user requests to create a role with permissions that are not valid and not supposed to be used ever.
|
switch (arg) {
|
||||||
* If we don't check for this, we can run into issues where functions like the `isAtLeastAsPrivileged` will not work as expected, because we compare the size of each permission set.
|
case "environment":
|
||||||
* If the permission set contains invalid permissions, the size will be different, and result in incorrect results.
|
return null;
|
||||||
*/
|
case "secretPath":
|
||||||
export const validateProjectPermissions = (permissions: unknown) => {
|
return null;
|
||||||
const parsedPermissions =
|
case "secretName":
|
||||||
typeof permissions === "string" ? (JSON.parse(permissions) as string[]) : (permissions as string[]);
|
return `${TableName.SecretV2}.key`;
|
||||||
|
case "secretTags":
|
||||||
const flattenedPermissions = [...parsedPermissions];
|
return `${TableName.SecretTag}.slug`;
|
||||||
|
default:
|
||||||
for (const perm of flattenedPermissions) {
|
throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` });
|
||||||
const [action, subject] = perm;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!fullProjectPermissionSet.find(
|
|
||||||
(currentPermission) => currentPermission[0] === action && currentPermission[1] === subject
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Permission action ${action} on subject ${subject} is not valid`,
|
|
||||||
name: "Create Role"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* eslint-enable */
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
|
|||||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
|
import { TIdentityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
|
||||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
@@ -51,6 +52,8 @@ type TSamlConfigServiceFactoryDep = {
|
|||||||
TOrgDALFactory,
|
TOrgDALFactory,
|
||||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
@@ -71,7 +74,8 @@ export const samlConfigServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
tokenService,
|
tokenService,
|
||||||
smtpService
|
smtpService,
|
||||||
|
identityMetadataDAL
|
||||||
}: TSamlConfigServiceFactoryDep) => {
|
}: TSamlConfigServiceFactoryDep) => {
|
||||||
const createSamlCfg = async ({
|
const createSamlCfg = async ({
|
||||||
cert,
|
cert,
|
||||||
@@ -332,7 +336,8 @@ export const samlConfigServiceFactory = ({
|
|||||||
lastName,
|
lastName,
|
||||||
authProvider,
|
authProvider,
|
||||||
orgId,
|
orgId,
|
||||||
relayState
|
relayState,
|
||||||
|
metadata
|
||||||
}: TSamlLoginDTO) => {
|
}: TSamlLoginDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
@@ -386,6 +391,21 @@ export const samlConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata && foundUser.id) {
|
||||||
|
await identityMetadataDAL.delete({ userId: foundUser.id, orgId }, tx);
|
||||||
|
if (metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
userId: foundUser.id,
|
||||||
|
orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return foundUser;
|
return foundUser;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -474,6 +494,20 @@ export const samlConfigServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata && newUser.id) {
|
||||||
|
await identityMetadataDAL.delete({ userId: newUser.id, orgId }, tx);
|
||||||
|
if (metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
userId: newUser?.id,
|
||||||
|
orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return newUser;
|
return newUser;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,4 +53,5 @@ export type TSamlLoginDTO = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
// saml thingy
|
// saml thingy
|
||||||
relayState?: string;
|
relayState?: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -360,7 +360,11 @@ export const ORGANIZATIONS = {
|
|||||||
organizationId: "The ID of the organization to update the membership for.",
|
organizationId: "The ID of the organization to update the membership for.",
|
||||||
membershipId: "The ID of the membership to update.",
|
membershipId: "The ID of the membership to update.",
|
||||||
role: "The new role of the membership.",
|
role: "The new role of the membership.",
|
||||||
isActive: "The active status of the membership"
|
isActive: "The active status of the membership",
|
||||||
|
metadata: {
|
||||||
|
key: "The key for user metadata tag.",
|
||||||
|
value: "The value for user metadata tag."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
DELETE_USER_MEMBERSHIP: {
|
DELETE_USER_MEMBERSHIP: {
|
||||||
organizationId: "The ID of the organization to delete the membership from.",
|
organizationId: "The ID of the organization to delete the membership from.",
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -52,3 +52,21 @@ export const unique = <T, K extends string | number | symbol>(array: readonly T[
|
|||||||
);
|
);
|
||||||
return Object.values(valueMap);
|
return Object.values(valueMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an array to a dictionary by mapping each item
|
||||||
|
* into a dictionary key & value
|
||||||
|
*/
|
||||||
|
export const objectify = <T, Key extends string | number | symbol, Value = T>(
|
||||||
|
array: readonly T[],
|
||||||
|
getKey: (item: T) => Key,
|
||||||
|
getValue: (item: T) => Value = (item) => item as unknown as Value
|
||||||
|
): Record<Key, Value> => {
|
||||||
|
return array.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
acc[getKey(item)] = getValue(item);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<Key, Value>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { UnauthorizedError } from "../errors";
|
||||||
|
|
||||||
|
type TKnexDynamicPrimitiveOperator = {
|
||||||
|
operator: "eq" | "ne" | "startsWith" | "endsWith";
|
||||||
|
value: string;
|
||||||
|
field: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TKnexDynamicInOperator = {
|
||||||
|
operator: "in";
|
||||||
|
value: string[] | number[];
|
||||||
|
field: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TKnexNonGroupOperator = TKnexDynamicInOperator | TKnexDynamicPrimitiveOperator;
|
||||||
|
|
||||||
|
type TKnexGroupOperator = {
|
||||||
|
operator: "and" | "or" | "not";
|
||||||
|
value: (TKnexNonGroupOperator | TKnexGroupOperator)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 = (dynamicQuery: TKnexDynamicOperator, rootQueryBuild: Knex.QueryBuilder) => {
|
||||||
|
const stack = [{ filterAst: dynamicQuery, queryBuilder: rootQueryBuild }];
|
||||||
|
|
||||||
|
while (stack.length) {
|
||||||
|
const { filterAst, queryBuilder } = stack.pop()!;
|
||||||
|
switch (filterAst.operator) {
|
||||||
|
case "eq": {
|
||||||
|
void queryBuilder.where(filterAst.field, "=", filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "ne": {
|
||||||
|
void queryBuilder.whereNot(filterAst.field, filterAst.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "startsWith": {
|
||||||
|
void queryBuilder.whereILike(filterAst.field, `${filterAst.value}%`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "endsWith": {
|
||||||
|
void queryBuilder.whereILike(filterAst.field, `%${filterAst.value}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "and": {
|
||||||
|
void queryBuilder.andWhere((subQueryBuilder) => {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "or": {
|
||||||
|
void queryBuilder.orWhere((subQueryBuilder) => {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "not": {
|
||||||
|
void queryBuilder.whereNot((subQueryBuilder) => {
|
||||||
|
filterAst.value.forEach((el) => {
|
||||||
|
stack.push({
|
||||||
|
queryBuilder: subQueryBuilder,
|
||||||
|
filterAst: el
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new UnauthorizedError({ message: `Invalid knex dynamic operator: ${filterAst.operator}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -101,6 +101,7 @@ import { groupProjectDALFactory } from "@app/services/group-project/group-projec
|
|||||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
|
import { identityMetadataDALFactory } from "@app/services/identity/identity-metadata-dal";
|
||||||
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||||
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
||||||
@@ -265,6 +266,7 @@ export const registerRoutes = async (
|
|||||||
const serviceTokenDAL = serviceTokenDALFactory(db);
|
const serviceTokenDAL = serviceTokenDALFactory(db);
|
||||||
|
|
||||||
const identityDAL = identityDALFactory(db);
|
const identityDAL = identityDALFactory(db);
|
||||||
|
const identityMetadataDAL = identityMetadataDALFactory(db);
|
||||||
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
|
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
|
||||||
const identityOrgMembershipDAL = identityOrgDALFactory(db);
|
const identityOrgMembershipDAL = identityOrgDALFactory(db);
|
||||||
const identityProjectDAL = identityProjectDALFactory(db);
|
const identityProjectDAL = identityProjectDALFactory(db);
|
||||||
@@ -386,6 +388,7 @@ export const registerRoutes = async (
|
|||||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
||||||
|
|
||||||
const samlService = samlConfigServiceFactory({
|
const samlService = samlConfigServiceFactory({
|
||||||
|
identityMetadataDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
@@ -489,6 +492,7 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
const orgService = orgServiceFactory({
|
const orgService = orgServiceFactory({
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
|
identityMetadataDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@@ -1027,7 +1031,8 @@ export const registerRoutes = async (
|
|||||||
identityDAL,
|
identityDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
identityMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
const identityAccessTokenService = identityAccessTokenServiceFactory({
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (includeDynamicSecrets) {
|
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,
|
||||||
@@ -241,7 +241,7 @@ 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,
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().describe(IDENTITIES.CREATE.name),
|
name: z.string().trim().describe(IDENTITIES.CREATE.name),
|
||||||
organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
|
organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
|
||||||
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role)
|
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role),
|
||||||
|
metadata: z
|
||||||
|
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -93,7 +97,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
|
name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
|
||||||
role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role)
|
role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role),
|
||||||
|
metadata: z
|
||||||
|
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -193,6 +201,14 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identity: IdentityOrgMembershipsSchema.extend({
|
identity: IdentityOrgMembershipsSchema.extend({
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
key: z.string().trim().min(1),
|
||||||
|
id: z.string().trim().min(1),
|
||||||
|
value: z.string().trim().min(1)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
customRole: OrgRolesSchema.pick({
|
customRole: OrgRolesSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
|||||||
@@ -130,18 +130,24 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
membership: OrgMembershipsSchema.merge(
|
membership: OrgMembershipsSchema.extend({
|
||||||
z.object({
|
metadata: z
|
||||||
user: UsersSchema.pick({
|
.object({
|
||||||
username: true,
|
key: z.string().trim().min(1),
|
||||||
email: true,
|
id: z.string().trim().min(1),
|
||||||
isEmailVerified: true,
|
value: z.string().trim().min(1)
|
||||||
firstName: true,
|
})
|
||||||
lastName: true,
|
.array()
|
||||||
id: true
|
.optional(),
|
||||||
}).merge(z.object({ publicKey: z.string().nullable() }))
|
user: UsersSchema.pick({
|
||||||
})
|
username: true,
|
||||||
).omit({ createdAt: true, updatedAt: true })
|
email: true,
|
||||||
|
isEmailVerified: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
}).extend({ publicKey: z.string().nullable() })
|
||||||
|
}).omit({ createdAt: true, updatedAt: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -178,7 +184,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
role: z.string().trim().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role),
|
role: z.string().trim().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.role),
|
||||||
isActive: z.boolean().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.isActive)
|
isActive: z.boolean().optional().describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.isActive),
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
key: z.string().trim().min(1).describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.metadata.key),
|
||||||
|
value: z.string().trim().min(1).describe(ORGANIZATIONS.UPDATE_USER_MEMBERSHIP.metadata.value)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIdentityMetadataDALFactory = ReturnType<typeof identityMetadataDALFactory>;
|
||||||
|
|
||||||
|
export const identityMetadataDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.IdentityMetadata);
|
||||||
|
return orm;
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { Knex } from "knex";
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
||||||
|
|
||||||
@@ -42,10 +42,25 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
const paginatedFetchIdentity = (tx || db.replicaNode())(TableName.Identity)
|
||||||
|
.where((queryBuilder) => {
|
||||||
|
if (limit) {
|
||||||
|
void queryBuilder.offset(offset).limit(limit);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.as(TableName.Identity);
|
||||||
|
|
||||||
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||||
.where(filter)
|
.where(filter)
|
||||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
.join<Awaited<typeof paginatedFetchIdentity>>(paginatedFetchIdentity, (queryBuilder) => {
|
||||||
|
queryBuilder.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`);
|
||||||
|
})
|
||||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`)
|
||||||
|
.andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
||||||
// cr stands for custom role
|
// cr stands for custom role
|
||||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||||
@@ -55,12 +70,15 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
||||||
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
.select(db.ref("permissions").as("crPermission").withSchema(TableName.OrgRoles))
|
||||||
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
|
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
|
||||||
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
|
.select(
|
||||||
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity));
|
db.ref("name").as("identityName").withSchema(TableName.Identity),
|
||||||
|
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity)
|
||||||
if (limit) {
|
)
|
||||||
void query.offset(offset).limit(limit);
|
.select(
|
||||||
}
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
|
);
|
||||||
|
|
||||||
if (orderBy) {
|
if (orderBy) {
|
||||||
switch (orderBy) {
|
switch (orderBy) {
|
||||||
@@ -80,9 +98,10 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const docs = await query;
|
const docs = await query;
|
||||||
|
const formattedDocs = sqlNestRelationships({
|
||||||
return docs.map(
|
data: docs,
|
||||||
({
|
key: "id",
|
||||||
|
parentMapper: ({
|
||||||
crId,
|
crId,
|
||||||
crDescription,
|
crDescription,
|
||||||
crSlug,
|
crSlug,
|
||||||
@@ -91,16 +110,21 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
identityId,
|
identityId,
|
||||||
identityName,
|
identityName,
|
||||||
identityAuthMethod,
|
identityAuthMethod,
|
||||||
...el
|
role,
|
||||||
|
roleId,
|
||||||
|
id,
|
||||||
|
orgId,
|
||||||
|
createdAt,
|
||||||
|
updatedAt
|
||||||
}) => ({
|
}) => ({
|
||||||
...el,
|
role,
|
||||||
|
roleId,
|
||||||
identityId,
|
identityId,
|
||||||
identity: {
|
id,
|
||||||
id: identityId,
|
orgId,
|
||||||
name: identityName,
|
createdAt,
|
||||||
authMethod: identityAuthMethod
|
updatedAt,
|
||||||
},
|
customRole: roleId
|
||||||
customRole: el.roleId
|
|
||||||
? {
|
? {
|
||||||
id: crId,
|
id: crId,
|
||||||
name: crName,
|
name: crName,
|
||||||
@@ -108,9 +132,27 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
permissions: crPermission,
|
permissions: crPermission,
|
||||||
description: crDescription
|
description: crDescription
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined,
|
||||||
})
|
identity: {
|
||||||
);
|
id: identityId,
|
||||||
|
name: identityName,
|
||||||
|
authMethod: identityAuthMethod as string
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedDocs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindByOrgId" });
|
throw new DatabaseError({ error, name: "FindByOrgId" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { TIdentityProjectDALFactory } from "@app/services/identity-project/ident
|
|||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "./identity-dal";
|
import { TIdentityDALFactory } from "./identity-dal";
|
||||||
|
import { TIdentityMetadataDALFactory } from "./identity-metadata-dal";
|
||||||
import { TIdentityOrgDALFactory } from "./identity-org-dal";
|
import { TIdentityOrgDALFactory } from "./identity-org-dal";
|
||||||
import {
|
import {
|
||||||
TCreateIdentityDTO,
|
TCreateIdentityDTO,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
|
|
||||||
type TIdentityServiceFactoryDep = {
|
type TIdentityServiceFactoryDep = {
|
||||||
identityDAL: TIdentityDALFactory;
|
identityDAL: TIdentityDALFactory;
|
||||||
|
identityMetadataDAL: TIdentityMetadataDALFactory;
|
||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
|
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
@@ -32,6 +34,7 @@ export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
|||||||
|
|
||||||
export const identityServiceFactory = ({
|
export const identityServiceFactory = ({
|
||||||
identityDAL,
|
identityDAL,
|
||||||
|
identityMetadataDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -44,7 +47,8 @@ export const identityServiceFactory = ({
|
|||||||
orgId,
|
orgId,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
metadata
|
||||||
}: TCreateIdentityDTO) => {
|
}: TCreateIdentityDTO) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||||
@@ -78,6 +82,17 @@ export const identityServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
if (metadata && metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
identityId: newIdentity.id,
|
||||||
|
orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
return newIdentity;
|
return newIdentity;
|
||||||
});
|
});
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
@@ -92,7 +107,8 @@ export const identityServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
metadata
|
||||||
}: TUpdateIdentityDTO) => {
|
}: TUpdateIdentityDTO) => {
|
||||||
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
|
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
|
||||||
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });
|
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });
|
||||||
@@ -134,8 +150,8 @@ export const identityServiceFactory = ({
|
|||||||
const identity = await identityDAL.transaction(async (tx) => {
|
const identity = await identityDAL.transaction(async (tx) => {
|
||||||
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
|
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
|
||||||
if (role) {
|
if (role) {
|
||||||
await identityOrgMembershipDAL.update(
|
await identityOrgMembershipDAL.updateById(
|
||||||
{ identityId: id },
|
identityOrgMembership.id,
|
||||||
{
|
{
|
||||||
role: customRole ? OrgMembershipRole.Custom : role,
|
role: customRole ? OrgMembershipRole.Custom : role,
|
||||||
roleId: customRole?.id || null
|
roleId: customRole?.id || null
|
||||||
@@ -143,6 +159,20 @@ export const identityServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (metadata) {
|
||||||
|
await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx);
|
||||||
|
if (metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
identityId: newIdentity.id,
|
||||||
|
orgId: identityOrgMembership.orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return newIdentity;
|
return newIdentity;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
|||||||
export type TCreateIdentityDTO = {
|
export type TCreateIdentityDTO = {
|
||||||
role: string;
|
role: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
export type TUpdateIdentityDTO = {
|
export type TUpdateIdentityDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TDeleteIdentityDTO = {
|
export type TDeleteIdentityDTO = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>;
|
export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>;
|
||||||
|
|
||||||
@@ -19,6 +19,11 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.UserEncryptionKey}.userId`,
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
`${TableName.Users}.id`
|
`${TableName.Users}.id`
|
||||||
)
|
)
|
||||||
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.OrgMembership}.userId`, `${TableName.IdentityMetadata}.userId`)
|
||||||
|
.andOn(`${TableName.OrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||||
|
})
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.OrgMembership),
|
db.ref("id").withSchema(TableName.OrgMembership),
|
||||||
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||||
@@ -33,19 +38,66 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("lastName").withSchema(TableName.Users),
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
db.ref("isEmailVerified").withSchema(TableName.Users),
|
db.ref("isEmailVerified").withSchema(TableName.Users),
|
||||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey),
|
||||||
|
db.ref("id").withSchema(TableName.IdentityMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue")
|
||||||
)
|
)
|
||||||
.where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
|
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!member) return undefined;
|
if (!member) return undefined;
|
||||||
|
|
||||||
const { email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data } = member;
|
const doc = sqlNestRelationships({
|
||||||
|
data: member,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: ({
|
||||||
|
email,
|
||||||
|
isEmailVerified,
|
||||||
|
username,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
userId,
|
||||||
|
publicKey,
|
||||||
|
roleId,
|
||||||
|
orgId,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
isActive,
|
||||||
|
inviteEmail
|
||||||
|
}) => ({
|
||||||
|
roleId,
|
||||||
|
orgId,
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
isActive,
|
||||||
|
inviteEmail,
|
||||||
|
user: {
|
||||||
|
id: userId,
|
||||||
|
email,
|
||||||
|
isEmailVerified,
|
||||||
|
username,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
userId,
|
||||||
|
publicKey
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return doc?.[0];
|
||||||
...data,
|
|
||||||
user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey }
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find org membership by id" });
|
throw new DatabaseError({ error, name: "Find org membership by id" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
|||||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
|
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
@@ -72,12 +73,13 @@ type TOrgServiceFactoryDep = {
|
|||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
groupDAL: TGroupDALFactory;
|
groupDAL: TGroupDALFactory;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
|
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
||||||
projectMembershipDAL: Pick<
|
projectMembershipDAL: Pick<
|
||||||
TProjectMembershipDALFactory,
|
TProjectMembershipDALFactory,
|
||||||
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
||||||
>;
|
>;
|
||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
|
||||||
incidentContactDAL: TIncidentContactsDALFactory;
|
incidentContactDAL: TIncidentContactsDALFactory;
|
||||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
@@ -115,7 +117,8 @@ export const orgServiceFactory = ({
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectUserMembershipRoleDAL
|
projectUserMembershipRoleDAL,
|
||||||
|
identityMetadataDAL
|
||||||
}: TOrgServiceFactoryDep) => {
|
}: TOrgServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Get organization details by the organization id
|
* Get organization details by the organization id
|
||||||
@@ -404,20 +407,22 @@ export const orgServiceFactory = ({
|
|||||||
userId,
|
userId,
|
||||||
membershipId,
|
membershipId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
metadata
|
||||||
}: TUpdateOrgMembershipDTO) => {
|
}: TUpdateOrgMembershipDTO) => {
|
||||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
const foundMembership = await orgMembershipDAL.findOne({
|
const foundMembership = await orgMembershipDAL.findById(membershipId);
|
||||||
id: membershipId,
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" });
|
if (!foundMembership) throw new NotFoundError({ message: "Failed to find organization membership" });
|
||||||
|
if (foundMembership.orgId !== orgId)
|
||||||
|
throw new UnauthorizedError({ message: "Updated org member doesn't belong to the organization" });
|
||||||
if (foundMembership.userId === userId)
|
if (foundMembership.userId === userId)
|
||||||
throw new UnauthorizedError({ message: "Cannot update own organization membership" });
|
throw new UnauthorizedError({ message: "Cannot update own organization membership" });
|
||||||
|
|
||||||
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
||||||
|
let userRole = role;
|
||||||
|
let userRoleId: string | null = null;
|
||||||
if (role && isCustomRole) {
|
if (role && isCustomRole) {
|
||||||
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
||||||
if (!customRole) throw new BadRequestError({ name: "UpdateMembership", message: "Organization role not found" });
|
if (!customRole) throw new BadRequestError({ name: "UpdateMembership", message: "Organization role not found" });
|
||||||
@@ -428,17 +433,31 @@ export const orgServiceFactory = ({
|
|||||||
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||||
});
|
});
|
||||||
|
|
||||||
const [membership] = await orgDAL.updateMembership(
|
userRole = OrgMembershipRole.Custom;
|
||||||
{ id: membershipId, orgId },
|
userRoleId = customRole.id;
|
||||||
{
|
|
||||||
role: OrgMembershipRole.Custom,
|
|
||||||
roleId: customRole.id
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return membership;
|
|
||||||
}
|
}
|
||||||
|
const membership = await orgDAL.transaction(async (tx) => {
|
||||||
|
const [updatedOrgMembership] = await orgDAL.updateMembership(
|
||||||
|
{ id: membershipId, orgId },
|
||||||
|
{ role: userRole, roleId: userRoleId, isActive }
|
||||||
|
);
|
||||||
|
|
||||||
const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null, isActive });
|
if (metadata) {
|
||||||
|
await identityMetadataDAL.delete({ userId: updatedOrgMembership.userId, orgId }, tx);
|
||||||
|
if (metadata.length) {
|
||||||
|
await identityMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
userId: updatedOrgMembership.userId,
|
||||||
|
orgId,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedOrgMembership;
|
||||||
|
});
|
||||||
return membership;
|
return membership;
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type TUpdateOrgMembershipDTO = {
|
|||||||
role?: string;
|
role?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
actorOrgId: string | undefined;
|
actorOrgId: string | undefined;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSet,
|
ProjectPermissionSet,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub
|
||||||
validateProjectPermissions
|
|
||||||
} 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";
|
||||||
|
|
||||||
@@ -60,8 +59,6 @@ export const projectRoleServiceFactory = ({
|
|||||||
throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" });
|
throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
validateProjectPermissions(data.permissions);
|
|
||||||
|
|
||||||
const role = await projectRoleDAL.create({
|
const role = await projectRoleDAL.create({
|
||||||
...data,
|
...data,
|
||||||
projectId
|
projectId
|
||||||
@@ -127,10 +124,6 @@ export const projectRoleServiceFactory = ({
|
|||||||
throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" });
|
throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.permissions) {
|
|
||||||
validateProjectPermissions(data.permissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [updatedRole] = await projectRoleDAL.update(
|
const [updatedRole] = await projectRoleDAL.update(
|
||||||
{ id: roleId, projectId },
|
{ id: roleId, projectId },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,30 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PermissionConditionOperators {
|
||||||
|
$IN = "$in",
|
||||||
|
$ALL = "$all",
|
||||||
|
$REGEX = "$regex",
|
||||||
|
$EQ = "$eq",
|
||||||
|
$NEQ = "$neq",
|
||||||
|
$GLOB = "$glob"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TPermissionConditionOperators = {
|
||||||
|
[PermissionConditionOperators.$IN]: string[];
|
||||||
|
[PermissionConditionOperators.$ALL]: string[];
|
||||||
|
[PermissionConditionOperators.$EQ]: string;
|
||||||
|
[PermissionConditionOperators.$NEQ]: string;
|
||||||
|
[PermissionConditionOperators.$REGEX]: string;
|
||||||
|
[PermissionConditionOperators.$GLOB]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPermissionCondition = Record<
|
||||||
|
string,
|
||||||
|
| string
|
||||||
|
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $neq: string; $glob: string }
|
||||||
|
>;
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
|
|||||||
@@ -67,12 +67,13 @@ export const useCreateIdentity = () => {
|
|||||||
export const useUpdateIdentity = () => {
|
export const useUpdateIdentity = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<Identity, {}, UpdateIdentityDTO>({
|
return useMutation<Identity, {}, UpdateIdentityDTO>({
|
||||||
mutationFn: async ({ identityId, name, role }) => {
|
mutationFn: async ({ identityId, name, role, metadata }) => {
|
||||||
const {
|
const {
|
||||||
data: { identity }
|
data: { identity }
|
||||||
} = await apiRequest.patch(`/api/v1/identities/${identityId}`, {
|
} = await apiRequest.patch(`/api/v1/identities/${identityId}`, {
|
||||||
name,
|
name,
|
||||||
role
|
role,
|
||||||
|
metadata
|
||||||
});
|
});
|
||||||
|
|
||||||
return identity;
|
return identity;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type IdentityMembershipOrg = {
|
|||||||
id: string;
|
id: string;
|
||||||
identity: Identity;
|
identity: Identity;
|
||||||
organization: string;
|
organization: string;
|
||||||
|
metadata: { key: string; value: string; id: string }[];
|
||||||
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
||||||
customRole?: TOrgRole;
|
customRole?: TOrgRole;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -79,6 +80,7 @@ export type CreateIdentityDTO = {
|
|||||||
name: string;
|
name: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateIdentityDTO = {
|
export type UpdateIdentityDTO = {
|
||||||
@@ -86,6 +88,7 @@ export type UpdateIdentityDTO = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteIdentityDTO = {
|
export type DeleteIdentityDTO = {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export type TPermission = {
|
|||||||
|
|
||||||
export type TProjectPermission = {
|
export type TProjectPermission = {
|
||||||
conditions?: Record<string, any>;
|
conditions?: Record<string, any>;
|
||||||
action: string;
|
action: string | string[];
|
||||||
subject: string | string[];
|
subject: string | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -235,12 +235,13 @@ export const useUpdateOrgMembership = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, UpdateOrgMembershipDTO>({
|
return useMutation<{}, {}, UpdateOrgMembershipDTO>({
|
||||||
mutationFn: ({ organizationId, membershipId, role, isActive }) => {
|
mutationFn: ({ organizationId, membershipId, role, isActive, metadata }) => {
|
||||||
return apiRequest.patch(
|
return apiRequest.patch(
|
||||||
`/api/v2/organizations/${organizationId}/memberships/${membershipId}`,
|
`/api/v2/organizations/${organizationId}/memberships/${membershipId}`,
|
||||||
{
|
{
|
||||||
role,
|
role,
|
||||||
isActive
|
isActive,
|
||||||
|
metadata
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export type UserEnc = {
|
|||||||
|
|
||||||
export type OrgUser = {
|
export type OrgUser = {
|
||||||
id: string;
|
id: string;
|
||||||
|
metadata: { key: string; value: string; id: string }[];
|
||||||
user: {
|
user: {
|
||||||
username: string;
|
username: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
@@ -142,6 +143,7 @@ export type UpdateOrgMembershipDTO = {
|
|||||||
membershipId: string;
|
membershipId: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeletOrgMembershipDTO = {
|
export type DeletOrgMembershipDTO = {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCopy, faKey, faPencil } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { IconButton, Tooltip } from "@app/components/v2";
|
import { IconButton, Tag, Tooltip } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
import { useGetIdentityById } from "@app/hooks/api";
|
import { useGetIdentityById } from "@app/hooks/api";
|
||||||
@@ -40,7 +40,8 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
|||||||
identityId,
|
identityId,
|
||||||
name: data.identity.name,
|
name: data.identity.name,
|
||||||
role: data.role,
|
role: data.role,
|
||||||
customRole: data.customRole
|
customRole: data.customRole,
|
||||||
|
metadata: data.metadata
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -77,10 +78,38 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
|||||||
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
|
||||||
<p className="text-sm text-mineshaft-300">{data.identity.name}</p>
|
<p className="text-sm text-mineshaft-300">{data.identity.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
|
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
|
||||||
<p className="text-sm text-mineshaft-300">{data.role}</p>
|
<p className="text-sm text-mineshaft-300">{data.role}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">Metadata</p>
|
||||||
|
{data?.metadata?.length ? (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||||
|
{data.metadata?.map((el) => (
|
||||||
|
<div key={el.id} className="flex items-center">
|
||||||
|
<Tag
|
||||||
|
size="xs"
|
||||||
|
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
|
||||||
|
<div>{el.key}</div>
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
size="xs"
|
||||||
|
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
|
||||||
|
>
|
||||||
|
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
{el.value}
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-mineshaft-300">-</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
+90
-11
@@ -1,13 +1,17 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import * as yup from "yup";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@@ -22,14 +26,21 @@ import {
|
|||||||
} from "@app/hooks/api/identities";
|
} from "@app/hooks/api/identities";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = yup
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
name: yup.string().required("MI name is required"),
|
name: z.string(),
|
||||||
role: yup.string()
|
role: z.string(),
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
key: z.string().trim().min(1),
|
||||||
|
value: z.string().trim().min(1)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["identity"]>;
|
popUp: UsePopUpState<["identity"]>;
|
||||||
@@ -61,12 +72,17 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: yupResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: ""
|
name: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const metadataFormFields = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "metadata"
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const identity = popUp?.identity?.data as {
|
const identity = popUp?.identity?.data as {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
@@ -93,7 +109,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [popUp?.identity?.data, roles]);
|
}, [popUp?.identity?.data, roles]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, role }: FormData) => {
|
const onFormSubmit = async ({ name, role, metadata }: FormData) => {
|
||||||
try {
|
try {
|
||||||
const identity = popUp?.identity?.data as {
|
const identity = popUp?.identity?.data as {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
@@ -108,7 +124,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
identityId: identity.identityId,
|
identityId: identity.identityId,
|
||||||
name,
|
name,
|
||||||
role: role || undefined,
|
role: role || undefined,
|
||||||
organizationId: orgId
|
organizationId: orgId,
|
||||||
|
metadata
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpToggle("identity", false);
|
handlePopUpToggle("identity", false);
|
||||||
@@ -118,7 +135,8 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const { id: createdId } = await createMutateAsync({
|
const { id: createdId } = await createMutateAsync({
|
||||||
name,
|
name,
|
||||||
role: role || undefined,
|
role: role || undefined,
|
||||||
organizationId: orgId
|
organizationId: orgId,
|
||||||
|
metadata
|
||||||
});
|
});
|
||||||
|
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
@@ -207,6 +225,67 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
<FormLabel label="Metadata" />
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 flex flex-col space-y-2">
|
||||||
|
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||||
|
<div key={metadataFieldId} className="flex items-end space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`metadata.${i}.key`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
{i === 0 && (
|
||||||
|
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`metadata.${i}.value`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete key"
|
||||||
|
className="bottom-0.5 h-9"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => metadataFormFields.remove(i)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="mt-2 flex justify-end">
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
||||||
|
>
|
||||||
|
Add Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import {
|
|||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faCircleXmark,
|
faCircleXmark,
|
||||||
faCopy,
|
faCopy,
|
||||||
|
faKey,
|
||||||
faPencil
|
faPencil
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, IconButton, Tooltip } from "@app/components/v2";
|
import { Button, IconButton, Tag, Tooltip } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
@@ -98,7 +99,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("orgMembership", {
|
handlePopUpOpen("orgMembership", {
|
||||||
membershipId: membership.id,
|
membershipId: membership.id,
|
||||||
role: membership.role
|
role: membership.role,
|
||||||
|
metadata: membership.metadata
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -162,10 +164,38 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
|||||||
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
|
<p className="text-sm font-semibold text-mineshaft-300">Organization Role</p>
|
||||||
<p className="text-sm text-mineshaft-300">{roleName ?? "-"}</p>
|
<p className="text-sm text-mineshaft-300">{roleName ?? "-"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
|
<p className="text-sm font-semibold text-mineshaft-300">Status</p>
|
||||||
<p className="text-sm text-mineshaft-300">{getStatus(membership)}</p>
|
<p className="text-sm text-mineshaft-300">{getStatus(membership)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-mineshaft-300">Metadata</p>
|
||||||
|
{membership?.metadata?.length ? (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
|
||||||
|
{membership.metadata?.map((el) => (
|
||||||
|
<div key={el.id} className="flex items-center">
|
||||||
|
<Tag
|
||||||
|
size="xs"
|
||||||
|
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
|
||||||
|
<div>{el.key}</div>
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
size="xs"
|
||||||
|
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
|
||||||
|
>
|
||||||
|
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
{el.value}
|
||||||
|
</div>
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-mineshaft-300">-</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{membership.isActive &&
|
{membership.isActive &&
|
||||||
(membership.status === "invited" || membership.status === "verified") &&
|
(membership.status === "invited" || membership.status === "verified") &&
|
||||||
membership.user.email &&
|
membership.user.email &&
|
||||||
|
|||||||
@@ -1,16 +1,35 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import { useGetOrgRoles, useUpdateOrgMembership } from "@app/hooks/api";
|
import { useGetOrgRoles, useUpdateOrgMembership } from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
role: z.string()
|
role: z.string(),
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
key: z.string().trim().min(1),
|
||||||
|
value: z.string().trim().min(1)
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
@@ -39,9 +58,15 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
resolver: zodResolver(schema)
|
resolver: zodResolver(schema)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const metadataFormFields = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "metadata"
|
||||||
|
});
|
||||||
|
|
||||||
const popUpData = popUp?.orgMembership?.data as {
|
const popUpData = popUp?.orgMembership?.data as {
|
||||||
membershipId: string;
|
membershipId: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
metadata: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -49,7 +74,8 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
|
|
||||||
if (popUpData) {
|
if (popUpData) {
|
||||||
reset({
|
reset({
|
||||||
role: popUpData.role
|
role: popUpData.role,
|
||||||
|
metadata: popUpData.metadata
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
@@ -58,14 +84,15 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
}
|
}
|
||||||
}, [popUp?.orgMembership?.data, roles]);
|
}, [popUp?.orgMembership?.data, roles]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ role }: FormData) => {
|
const onFormSubmit = async ({ role, metadata }: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!orgId) return;
|
if (!orgId) return;
|
||||||
|
|
||||||
await updateOrgMembership({
|
await updateOrgMembership({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
membershipId: popUpData.membershipId,
|
membershipId: popUpData.membershipId,
|
||||||
role
|
role,
|
||||||
|
metadata
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpToggle("orgMembership", false);
|
handlePopUpToggle("orgMembership", false);
|
||||||
@@ -135,6 +162,67 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
<FormLabel label="Metadata" />
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 flex flex-col space-y-2">
|
||||||
|
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||||
|
<div key={metadataFieldId} className="flex items-end space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`metadata.${i}.key`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
{i === 0 && (
|
||||||
|
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`metadata.${i}.value`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete key"
|
||||||
|
className="bottom-0.5 h-9"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => metadataFormFields.remove(i)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="mt-2 flex justify-end">
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
||||||
|
>
|
||||||
|
Add Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
import { withProjectPermission } from "@app/hoc";
|
||||||
import { useDeleteProjectRole,useGetProjectRoleBySlug } from "@app/hooks/api";
|
import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { TabSections } from "../Types";
|
import { TabSections } from "../Types";
|
||||||
@@ -76,7 +76,9 @@ export const RolePage = withProjectPermission(
|
|||||||
variant="link"
|
variant="link"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||||
onClick={() => router.push(`/project/${projectId}/members?selectedTab=${TabSections.Roles}`)}
|
onClick={() =>
|
||||||
|
router.push(`/project/${projectId}/members?selectedTab=${TabSections.Roles}`)
|
||||||
|
}
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
Roles
|
Roles
|
||||||
@@ -139,7 +141,7 @@ export const RolePage = withProjectPermission(
|
|||||||
<div className="mr-4 w-96">
|
<div className="mr-4 w-96">
|
||||||
<RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} />
|
<RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} />
|
||||||
</div>
|
</div>
|
||||||
<RolePermissionsSection roleSlug={roleSlug} />
|
<RolePermissionsSection roleSlug={roleSlug} isDisabled={!isCustomRole} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { EmptyState } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
// This is made into seperate component because watch subscribes to all permissions
|
||||||
|
// thus keeping in top level casues render on all ones
|
||||||
|
export const PermissionEmptyState = () => {
|
||||||
|
const { watch } = useFormContext<TFormSchema>();
|
||||||
|
const isNotEmptyPermissions = Object.entries(watch("permissions") || {}).some(
|
||||||
|
([key, value]) => key && value?.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNotEmptyPermissions) return <div />;
|
||||||
|
|
||||||
|
return <EmptyState title="No policies applied" className="py-8" />;
|
||||||
|
};
|
||||||
+420
-126
@@ -1,29 +1,39 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
|
import {
|
||||||
|
PermissionConditionOperators,
|
||||||
|
TPermissionCondition,
|
||||||
|
TPermissionConditionOperators
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { TProjectPermission } from "@app/hooks/api/roles/types";
|
import { TProjectPermission } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
const generalPermissionSchema = z
|
const GeneralPolicyActionSchema = z.object({
|
||||||
.object({
|
read: z.boolean().optional(),
|
||||||
read: z.boolean().optional(),
|
edit: z.boolean().optional(),
|
||||||
edit: z.boolean().optional(),
|
delete: z.boolean().optional(),
|
||||||
delete: z.boolean().optional(),
|
create: z.boolean().optional()
|
||||||
create: z.boolean().optional()
|
});
|
||||||
})
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
const multiEnvPermissionSchema = z
|
const SecretFolderPolicyActionSchema = z.object({
|
||||||
.object({
|
read: z.boolean().optional()
|
||||||
secretPath: z.string().trim().optional(),
|
});
|
||||||
read: z.boolean().optional(),
|
|
||||||
edit: z.boolean().optional(),
|
|
||||||
delete: z.boolean().optional(),
|
|
||||||
create: z.boolean().optional()
|
|
||||||
})
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
const PERMISSION_ACTIONS = ["read", "create", "edit", "delete"] as const;
|
const SecretRollbackPolicyActionSchema = z.object({
|
||||||
|
read: z.boolean().optional(),
|
||||||
|
create: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const WorkspacePolicyActionSchema = z.object({
|
||||||
|
edit: z.boolean().optional(),
|
||||||
|
delete: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConditionSchema = z.object({
|
||||||
|
operator: z.string(),
|
||||||
|
lhs: z.string(),
|
||||||
|
rhs: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
@@ -35,139 +45,423 @@ export const formSchema = z.object({
|
|||||||
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
|
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
|
||||||
permissions: z
|
permissions: z
|
||||||
.object({
|
.object({
|
||||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
||||||
"secret-folders": generalPermissionSchema.optional(),
|
conditions: ConditionSchema.array()
|
||||||
member: generalPermissionSchema,
|
.optional()
|
||||||
groups: generalPermissionSchema,
|
.default([])
|
||||||
identity: generalPermissionSchema,
|
.refine(
|
||||||
role: generalPermissionSchema,
|
(el) => {
|
||||||
integrations: generalPermissionSchema,
|
const lhsOperatorSet = new Set<string>();
|
||||||
webhooks: generalPermissionSchema,
|
for (let i = 0; i < el.length; i += 1) {
|
||||||
"service-tokens": generalPermissionSchema,
|
const { lhs, operator } = el[i];
|
||||||
settings: generalPermissionSchema,
|
if (lhsOperatorSet.has(`${lhs}-${operator}`)) {
|
||||||
environments: generalPermissionSchema,
|
return false;
|
||||||
tags: generalPermissionSchema,
|
}
|
||||||
"ip-allowlist": generalPermissionSchema,
|
lhsOperatorSet.add(`${lhs}-${operator}`);
|
||||||
"certificate-authorities": generalPermissionSchema,
|
}
|
||||||
certificates: generalPermissionSchema,
|
return true;
|
||||||
"pki-alerts": generalPermissionSchema,
|
},
|
||||||
"pki-collections": generalPermissionSchema,
|
{ message: "Duplicate operator found for a condition" }
|
||||||
"certificate-templates": generalPermissionSchema,
|
)
|
||||||
// akhilmhdh: refactor all keys like below
|
})
|
||||||
[ProjectPermissionSub.SecretApproval]: generalPermissionSchema,
|
.array()
|
||||||
workspace: z
|
.default([]),
|
||||||
.object({
|
[ProjectPermissionSub.SecretFolders]: SecretFolderPolicyActionSchema.array().default([]),
|
||||||
edit: z.boolean().optional(),
|
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]),
|
||||||
delete: z.boolean().optional()
|
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]),
|
||||||
})
|
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]),
|
||||||
.optional(),
|
[ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]),
|
||||||
"secret-rollback": z
|
[ProjectPermissionSub.Integrations]: GeneralPolicyActionSchema.array().default([]),
|
||||||
.object({
|
[ProjectPermissionSub.Webhooks]: GeneralPolicyActionSchema.array().default([]),
|
||||||
read: z.boolean().optional(),
|
[ProjectPermissionSub.ServiceTokens]: GeneralPolicyActionSchema.array().default([]),
|
||||||
create: z.boolean().optional()
|
[ProjectPermissionSub.Settings]: GeneralPolicyActionSchema.array().default([]),
|
||||||
})
|
[ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]),
|
||||||
.optional()
|
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([])
|
||||||
})
|
})
|
||||||
|
.partial()
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TFormSchema = z.infer<typeof formSchema>;
|
export type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
const multiEnvApi2Form = (
|
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||||
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
const formConditions: z.infer<typeof ConditionSchema>[] = [];
|
||||||
permission: TProjectPermission
|
Object.entries(caslConditions).forEach(([type, condition]) => {
|
||||||
) => {
|
if (typeof condition === "string") {
|
||||||
const isCustomRule = Boolean(permission?.conditions?.environment);
|
formConditions.push({
|
||||||
// full access
|
operator: PermissionConditionOperators.$EQ,
|
||||||
if (isCustomRule && formVal && !formVal?.custom) {
|
lhs: type,
|
||||||
formVal.custom = { read: true, edit: true, delete: true, create: true };
|
rhs: condition
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
const secretEnv = permission?.conditions?.environment || "all";
|
Object.keys(condition).forEach((conditionOperator) => {
|
||||||
const secretPath = permission?.conditions?.secretPath?.$glob;
|
const rhs = condition[conditionOperator as PermissionConditionOperators];
|
||||||
// initialize
|
formConditions.push({
|
||||||
if (formVal && !formVal?.[secretEnv]) {
|
operator: conditionOperator,
|
||||||
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
|
lhs: type,
|
||||||
}
|
rhs: typeof rhs === "string" ? rhs : rhs.join(",")
|
||||||
|
});
|
||||||
formVal[secretEnv][permission.action] = true;
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return formConditions;
|
||||||
};
|
};
|
||||||
|
|
||||||
// convert role permission to form compatiable data structure
|
// convert role permission to form compatiable data structure
|
||||||
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||||
// any because if it set it as form type due to the discriminated union type of ts
|
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||||
// i would have to write a if loop with both conditions same
|
|
||||||
const formVal: Record<string, any> = {};
|
|
||||||
|
|
||||||
permissions.forEach((permission) => {
|
permissions.forEach((permission) => {
|
||||||
const { subject: caslSub, action } = permission;
|
const { subject: caslSub, action, conditions } = permission;
|
||||||
const subject = typeof caslSub === "string" ? caslSub : caslSub[0];
|
const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub;
|
||||||
if (!formVal?.[subject]) formVal[subject] = {};
|
|
||||||
|
|
||||||
if (subject === "secrets") {
|
if (
|
||||||
multiEnvApi2Form(formVal[subject], permission);
|
[
|
||||||
} else {
|
ProjectPermissionSub.Secrets,
|
||||||
// everything else follows same pattern
|
ProjectPermissionSub.Member,
|
||||||
// formVal[settings][read | write] = true
|
ProjectPermissionSub.Groups,
|
||||||
formVal[subject][action] = true;
|
ProjectPermissionSub.Identity,
|
||||||
|
ProjectPermissionSub.Role,
|
||||||
|
ProjectPermissionSub.Integrations,
|
||||||
|
ProjectPermissionSub.Webhooks,
|
||||||
|
ProjectPermissionSub.ServiceTokens,
|
||||||
|
ProjectPermissionSub.Settings,
|
||||||
|
ProjectPermissionSub.Environments,
|
||||||
|
ProjectPermissionSub.AuditLogs,
|
||||||
|
ProjectPermissionSub.IpAllowList,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities,
|
||||||
|
ProjectPermissionSub.Certificates,
|
||||||
|
ProjectPermissionSub.PkiAlerts,
|
||||||
|
ProjectPermissionSub.PkiCollections,
|
||||||
|
ProjectPermissionSub.CertificateTemplates,
|
||||||
|
ProjectPermissionSub.SecretApproval,
|
||||||
|
ProjectPermissionSub.Tags,
|
||||||
|
ProjectPermissionSub.SecretRotation,
|
||||||
|
ProjectPermissionSub.Kms
|
||||||
|
].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
|
||||||
|
if (subject === ProjectPermissionSub.Secrets) {
|
||||||
|
if (!formVal[subject]) formVal[subject] = [];
|
||||||
|
formVal[subject]!.push({
|
||||||
|
read: canRead,
|
||||||
|
create: canCreate,
|
||||||
|
edit: canEdit,
|
||||||
|
delete: canDelete,
|
||||||
|
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// deduplicate multiple rules for other policies
|
||||||
|
// because they don't have condition it doesn't make sense for multiple rules
|
||||||
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||||
|
if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true;
|
||||||
|
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
||||||
|
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
|
||||||
|
}
|
||||||
|
} else if (subject === ProjectPermissionSub.Workspace) {
|
||||||
|
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||||
|
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||||
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
|
||||||
|
// from above statement we are sure it won't be undefined
|
||||||
|
if (canEdit) formVal[subject as ProjectPermissionSub.Workspace]![0].edit = true;
|
||||||
|
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
|
||||||
|
} else if (subject === ProjectPermissionSub.SecretRollback) {
|
||||||
|
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||||
|
const canCreate = action.includes(ProjectPermissionActions.Create);
|
||||||
|
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;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return formVal;
|
return formVal;
|
||||||
};
|
};
|
||||||
|
|
||||||
const multiEnvForm2Api = (
|
const convertFormOperatorToCaslCondition = (
|
||||||
permissions: TProjectPermission[],
|
conditions: { lhs: string; rhs: string; operator: string }[]
|
||||||
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
|
||||||
subject: "secrets"
|
|
||||||
) => {
|
) => {
|
||||||
if (!formVal) return;
|
const caslCondition: Record<string, Partial<TPermissionConditionOperators>> = {};
|
||||||
|
conditions.forEach((el) => {
|
||||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
if (!caslCondition[el.lhs]) caslCondition[el.lhs] = {};
|
||||||
// if any of them is set in all push it without any condition
|
if (
|
||||||
PERMISSION_ACTIONS.forEach((action) => {
|
el.operator === PermissionConditionOperators.$IN ||
|
||||||
if (formVal?.all?.[action]) permissions.push({ action, subject });
|
el.operator === PermissionConditionOperators.$ALL
|
||||||
|
) {
|
||||||
|
caslCondition[el.lhs][el.operator] = el.rhs.split(",");
|
||||||
|
} else {
|
||||||
|
caslCondition[el.lhs][
|
||||||
|
el.operator as Exclude<
|
||||||
|
PermissionConditionOperators,
|
||||||
|
PermissionConditionOperators.$ALL | PermissionConditionOperators.$IN
|
||||||
|
>
|
||||||
|
] = el.rhs;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
return caslCondition;
|
||||||
if (!isFullAccess) {
|
|
||||||
Object.keys(formVal || {})
|
|
||||||
.filter((id) => id !== "all" && id !== "custom") // remove all and custom for iter
|
|
||||||
.forEach((slug) => {
|
|
||||||
const actions = Object.keys(formVal?.[slug] || {}) as [
|
|
||||||
"read",
|
|
||||||
"edit",
|
|
||||||
"create",
|
|
||||||
"delete",
|
|
||||||
"secretPath"
|
|
||||||
];
|
|
||||||
actions.forEach((action) => {
|
|
||||||
// if not full access for an action
|
|
||||||
if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) {
|
|
||||||
const conditions: Record<string, unknown> = { environment: slug };
|
|
||||||
if (formVal[slug]?.secretPath)
|
|
||||||
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
|
|
||||||
|
|
||||||
permissions.push({ action, subject, conditions });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||||
const permissions: TProjectPermission[] = [];
|
const permissions: TProjectPermission[] = [];
|
||||||
// other than workspace everything else follows same
|
// other than workspace everything else follows same
|
||||||
// if in future there is a different follow the above on how workspace is done
|
// if in future there is a different follow the above on how workspace is done
|
||||||
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
Object.entries(formVal || {}).forEach(([subject, rules]) => {
|
||||||
if (rule === "secrets") {
|
rules.forEach((actions) => {
|
||||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule);
|
const caslActions = Object.keys(actions).filter(
|
||||||
} else if (actions) {
|
(el) => actions?.[el as keyof typeof actions] && el !== "conditions"
|
||||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
);
|
||||||
if (isAllowed) {
|
const caslConditions =
|
||||||
permissions.push({ subject: rule, action });
|
"conditions" in actions
|
||||||
}
|
? convertFormOperatorToCaslCondition(actions.conditions)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
permissions.push({
|
||||||
|
action: caslActions,
|
||||||
|
subject,
|
||||||
|
conditions: caslConditions
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
return permissions;
|
return permissions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TProjectPermissionObject = {
|
||||||
|
[K in ProjectPermissionSub]: {
|
||||||
|
title: string;
|
||||||
|
actions: {
|
||||||
|
label: string;
|
||||||
|
value: keyof Omit<
|
||||||
|
NonNullable<NonNullable<TFormSchema["permissions"]>[K]>[number],
|
||||||
|
"conditions"
|
||||||
|
>;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||||
|
[ProjectPermissionSub.Secrets]: {
|
||||||
|
title: "Secrets",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.SecretFolders]: {
|
||||||
|
title: "Secret Folders",
|
||||||
|
actions: [{ label: "Read Only", value: "read" }]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Kms]: {
|
||||||
|
title: "KMS",
|
||||||
|
actions: [{ label: "Modify", value: "edit" }]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Integrations]: {
|
||||||
|
title: "Integrations",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Workspace]: {
|
||||||
|
title: "Project",
|
||||||
|
actions: [
|
||||||
|
{ label: "Update project details", value: "edit" },
|
||||||
|
{ label: "Delete project", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Role]: {
|
||||||
|
title: "Roles",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Member]: {
|
||||||
|
title: "User Management",
|
||||||
|
actions: [
|
||||||
|
{ label: "View all members", value: "read" },
|
||||||
|
{ label: "Invite members", value: "create" },
|
||||||
|
{ label: "Edit members", value: "edit" },
|
||||||
|
{ label: "Remove members", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Groups]: {
|
||||||
|
title: "Group Management",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Identity]: {
|
||||||
|
title: "Machine Identity Management",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Webhooks]: {
|
||||||
|
title: "Webhooks",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.ServiceTokens]: {
|
||||||
|
title: "Service Tokens",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Settings]: {
|
||||||
|
title: "Settings",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Modify", value: "edit" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Environments]: {
|
||||||
|
title: "Environments",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Tags]: {
|
||||||
|
title: "Tags",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.AuditLogs]: {
|
||||||
|
title: "Audit Logs",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.IpAllowList]: {
|
||||||
|
title: "IP Allowlist",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.CertificateAuthorities]: {
|
||||||
|
title: "Certificate Authorities",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Certificates]: {
|
||||||
|
title: "Certificates",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.CertificateTemplates]: {
|
||||||
|
title: "Certificate Templates",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.PkiCollections]: {
|
||||||
|
title: "PKI Collections",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.PkiAlerts]: {
|
||||||
|
title: "PKI Alerts",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.SecretApproval]: {
|
||||||
|
title: "Secret Protect policy",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.SecretRotation]: {
|
||||||
|
title: "Secret Rotation",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.SecretRollback]: {
|
||||||
|
title: "Secret Rollback",
|
||||||
|
actions: [
|
||||||
|
{ label: "Perform rollback", value: "create" },
|
||||||
|
{ label: "View", value: "read" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
-219
@@ -1,219 +0,0 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
|
||||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
const GENERAL_PERMISSIONS = [
|
|
||||||
{ action: "read", label: "View" },
|
|
||||||
{ action: "create", label: "Create" },
|
|
||||||
{ action: "edit", label: "Modify" },
|
|
||||||
{ action: "delete", label: "Remove" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const WORKSPACE_PERMISSIONS = [
|
|
||||||
{ action: "edit", label: "Update project details" },
|
|
||||||
{ action: "delete", label: "Delete projects" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const MEMBERS_PERMISSIONS = [
|
|
||||||
{ action: "read", label: "View all members" },
|
|
||||||
{ action: "create", label: "Invite members" },
|
|
||||||
{ action: "edit", label: "Edit members" },
|
|
||||||
{ action: "delete", label: "Remove members" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const SECRET_ROLLBACK_PERMISSIONS = [
|
|
||||||
{ action: "create", label: "Perform Rollback" },
|
|
||||||
{ action: "read", label: "View" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const getPermissionList = (option: Props["formName"]) => {
|
|
||||||
switch (option) {
|
|
||||||
case "workspace":
|
|
||||||
return WORKSPACE_PERMISSIONS;
|
|
||||||
case "member":
|
|
||||||
return MEMBERS_PERMISSIONS;
|
|
||||||
case "secret-rollback":
|
|
||||||
return SECRET_ROLLBACK_PERMISSIONS;
|
|
||||||
default:
|
|
||||||
return GENERAL_PERMISSIONS;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type PermissionName =
|
|
||||||
| `permissions.workspace.${"edit" | "delete"}`
|
|
||||||
| `permissions.secret-rollback.${"create" | "read"}`
|
|
||||||
| `permissions.${Exclude<
|
|
||||||
keyof NonNullable<TFormSchema["permissions"]>,
|
|
||||||
"workspace" | "secret-rollback" | "secrets"
|
|
||||||
>}.${"read" | "create" | "edit" | "delete"}`;
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isEditable: boolean;
|
|
||||||
title: string;
|
|
||||||
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "secrets">;
|
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
|
||||||
control: Control<TFormSchema>;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Permission {
|
|
||||||
NoAccess = "no-access",
|
|
||||||
ReadOnly = "read-only",
|
|
||||||
FullAccess = "full-acess",
|
|
||||||
Custom = "custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RolePermissionRow = ({ isEditable, title, formName, control, setValue }: Props) => {
|
|
||||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
|
||||||
const [isCustom, setIsCustom] = useToggle();
|
|
||||||
|
|
||||||
const rule = useWatch({
|
|
||||||
control,
|
|
||||||
name: `permissions.${formName}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedPermissionCategory = useMemo(() => {
|
|
||||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
|
||||||
|
|
||||||
switch (formName) {
|
|
||||||
default: {
|
|
||||||
const totalActions = GENERAL_PERMISSIONS.length;
|
|
||||||
const score = actions
|
|
||||||
.map((key) => (rule?.[key] ? 1 : 0))
|
|
||||||
.reduce((a, b) => a + b, 0 as number);
|
|
||||||
if (isCustom) return Permission.Custom;
|
|
||||||
if (score === 0) return Permission.NoAccess;
|
|
||||||
if (score === totalActions) return Permission.FullAccess;
|
|
||||||
if (rule && "read" in rule) {
|
|
||||||
if (score === 1 && rule?.read) return Permission.ReadOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Permission.Custom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [rule, isCustom]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
|
||||||
else setIsCustom.off();
|
|
||||||
}, [selectedPermissionCategory]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
|
||||||
if (isRowCustom) {
|
|
||||||
setIsRowExpanded.on();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePermissionChange = (val: Permission) => {
|
|
||||||
if (val === Permission.Custom) {
|
|
||||||
setIsRowExpanded.on();
|
|
||||||
setIsCustom.on();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsCustom.off();
|
|
||||||
|
|
||||||
switch (val) {
|
|
||||||
case Permission.NoAccess:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ read: false, edit: false, create: false, delete: false },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case Permission.FullAccess:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ read: true, edit: true, create: true, delete: true },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case Permission.ReadOnly:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ read: true, edit: false, create: false, delete: false },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ read: false, edit: false, create: false, delete: false },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tr
|
|
||||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
|
||||||
onClick={() => setIsRowExpanded.toggle()}
|
|
||||||
>
|
|
||||||
<Td>
|
|
||||||
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
|
||||||
</Td>
|
|
||||||
<Td>{title}</Td>
|
|
||||||
<Td>
|
|
||||||
<Select
|
|
||||||
value={selectedPermissionCategory}
|
|
||||||
className="w-40 bg-mineshaft-600"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={handlePermissionChange}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
>
|
|
||||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
|
||||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
{isRowExpanded && (
|
|
||||||
<Tr>
|
|
||||||
<Td
|
|
||||||
colSpan={3}
|
|
||||||
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
{getPermissionList(formName).map(({ action, label }) => {
|
|
||||||
const permissionName = `permissions.${formName}.${action}` as PermissionName;
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
name={permissionName}
|
|
||||||
key={permissionName}
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={(e) => {
|
|
||||||
if (!isEditable) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to update default role"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
field.onChange(e);
|
|
||||||
}}
|
|
||||||
id={permissionName}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Checkbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-71
@@ -1,71 +0,0 @@
|
|||||||
import { Control, UseFormSetValue, useWatch } from "react-hook-form";
|
|
||||||
|
|
||||||
import { Select, SelectItem, Td, Tr } from "@app/components/v2";
|
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
|
||||||
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isEditable: boolean;
|
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
|
||||||
control: Control<TFormSchema>;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Permission {
|
|
||||||
SameAsSecrets = "same-as-secrets",
|
|
||||||
ReadOnly = "read-only"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RowPermissionSecretFoldersRow = ({ isEditable, setValue, control }: Props) => {
|
|
||||||
const formName = ProjectPermissionSub.SecretFolders;
|
|
||||||
const rule = useWatch({
|
|
||||||
control,
|
|
||||||
name: `permissions.${formName}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedPermissionCategory =
|
|
||||||
rule !== undefined ? Permission.ReadOnly : Permission.SameAsSecrets;
|
|
||||||
|
|
||||||
const handlePermissionChange = (val: Permission) => {
|
|
||||||
if (!val) return;
|
|
||||||
switch (val) {
|
|
||||||
case Permission.SameAsSecrets: {
|
|
||||||
setValue(`permissions.${formName}`, undefined, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Read-only
|
|
||||||
default:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{
|
|
||||||
read: true,
|
|
||||||
edit: false,
|
|
||||||
create: false,
|
|
||||||
delete: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
shouldDirty: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr>
|
|
||||||
<Td />
|
|
||||||
<Td>Secret Folders</Td>
|
|
||||||
<Td>
|
|
||||||
<Select
|
|
||||||
value={selectedPermissionCategory}
|
|
||||||
className="w-40 bg-mineshaft-600"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={handlePermissionChange}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
>
|
|
||||||
<SelectItem value={Permission.SameAsSecrets}>Same as Secrets</SelectItem>
|
|
||||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-248
@@ -1,248 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { Control, Controller, UseFormGetValues, UseFormSetValue, useWatch } from "react-hook-form";
|
|
||||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples";
|
|
||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
FormControl,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Table,
|
|
||||||
TableContainer,
|
|
||||||
TBody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
THead,
|
|
||||||
Tr
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useWorkspace } from "@app/context";
|
|
||||||
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string;
|
|
||||||
formName: "secrets";
|
|
||||||
isEditable: boolean;
|
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
|
||||||
getValue: UseFormGetValues<TFormSchema>;
|
|
||||||
control: Control<TFormSchema>;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Permission {
|
|
||||||
NoAccess = "no-access",
|
|
||||||
ReadOnly = "read-only",
|
|
||||||
FullAccess = "full-acess",
|
|
||||||
Custom = "custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RowPermissionSecretsRow = ({
|
|
||||||
title,
|
|
||||||
formName,
|
|
||||||
isEditable,
|
|
||||||
setValue,
|
|
||||||
getValue,
|
|
||||||
control
|
|
||||||
}: Props) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const environments = currentWorkspace?.environments || [];
|
|
||||||
|
|
||||||
const customRule = useWatch({
|
|
||||||
control,
|
|
||||||
name: `permissions.${formName}.custom`
|
|
||||||
});
|
|
||||||
const isCustom = Boolean(customRule);
|
|
||||||
|
|
||||||
const allRule = useWatch({ control, name: `permissions.${formName}.all` });
|
|
||||||
|
|
||||||
const selectedPermissionCategory = useMemo(() => {
|
|
||||||
const { read, delete: del, edit, create } = allRule || {};
|
|
||||||
if (read && del && edit && create) return Permission.FullAccess;
|
|
||||||
if (read) return Permission.ReadOnly;
|
|
||||||
return Permission.NoAccess;
|
|
||||||
}, [allRule]);
|
|
||||||
|
|
||||||
const handlePermissionChange = (val: Permission) => {
|
|
||||||
if (!val) return;
|
|
||||||
switch (val) {
|
|
||||||
case Permission.NoAccess: {
|
|
||||||
const permissions = getValue("permissions");
|
|
||||||
if (permissions) delete permissions[formName];
|
|
||||||
setValue("permissions", permissions, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Permission.FullAccess:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ all: { read: true, edit: true, create: true, delete: true } },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case Permission.ReadOnly:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ all: { read: true, edit: false, create: false, delete: false } },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ custom: { read: false, edit: false, create: false, delete: false } },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tr>
|
|
||||||
<Td>{isCustom && <FontAwesomeIcon icon={faChevronDown} />}</Td>
|
|
||||||
<Td>{title}</Td>
|
|
||||||
<Td>
|
|
||||||
<Select
|
|
||||||
value={isCustom ? Permission.Custom : selectedPermissionCategory}
|
|
||||||
className="w-40 bg-mineshaft-600"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={handlePermissionChange}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
>
|
|
||||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
|
||||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
{isCustom && (
|
|
||||||
<Tr>
|
|
||||||
<Td
|
|
||||||
colSpan={3}
|
|
||||||
className={`bg-bunker-600 px-0 py-0 ${isCustom && " border-mineshaft-500 p-8"}`}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<TableContainer className="border-mineshaft-500">
|
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
<Tr>
|
|
||||||
<Th />
|
|
||||||
<Th className="min-w-[8rem]">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
Secret Path
|
|
||||||
<span className="text-xs normal-case">
|
|
||||||
<GlobPatternExamples />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Th>
|
|
||||||
<Th className="text-center">View</Th>
|
|
||||||
<Th className="text-center">Create</Th>
|
|
||||||
<Th className="text-center">Modify</Th>
|
|
||||||
<Th className="text-center">Delete</Th>
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{isCustom &&
|
|
||||||
environments.map(({ name, slug }) => (
|
|
||||||
<Tr key={`custom-role-project-secret-${slug}`}>
|
|
||||||
<Td>{name}</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
name={`permissions.${formName}.${slug}.secretPath`}
|
|
||||||
control={control}
|
|
||||||
defaultValue="/**"
|
|
||||||
render={({ field }) => (
|
|
||||||
/* eslint-disable-next-line no-template-curly-in-string */
|
|
||||||
<FormControl helperText="Supports glob path pattern string">
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
className="w-full overflow-ellipsis"
|
|
||||||
placeholder="Glob patterns are supported"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
name={`permissions.${formName}.${slug}.read`}
|
|
||||||
control={control}
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
id={`permissions.${formName}.${slug}.read`}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
name={`permissions.${formName}.${slug}.create`}
|
|
||||||
control={control}
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
id={`permissions.${formName}.${slug}.modify`}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
name={`permissions.${formName}.${slug}.edit`}
|
|
||||||
control={control}
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
id={`permissions.${formName}.${slug}.modify`}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
defaultValue={false}
|
|
||||||
name={`permissions.${formName}.${slug}.delete`}
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
id={`permissions.${formName}.${slug}.delete`}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+96
-159
@@ -1,127 +1,57 @@
|
|||||||
import { useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { faPlus, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
|
import { Button, Modal, ModalContent, ModalTrigger } from "@app/components/v2";
|
||||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions";
|
||||||
|
import { NewPermissionRule } from "./components/NewPermissionRule";
|
||||||
|
import { SecretPermissionConditions } from "./components/SecretPermissionConditions";
|
||||||
|
import { PermissionEmptyState } from "./PermissionEmptyState";
|
||||||
import {
|
import {
|
||||||
formRolePermission2API,
|
formRolePermission2API,
|
||||||
formSchema,
|
formSchema,
|
||||||
|
PROJECT_PERMISSION_OBJECT,
|
||||||
rolePermission2Form,
|
rolePermission2Form,
|
||||||
TFormSchema
|
TFormSchema
|
||||||
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
} from "./ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
import { RolePermissionRow } from "./RolePermissionRow";
|
|
||||||
import { RowPermissionSecretFoldersRow } from "./RolePermissionSecretFoldersRow";
|
|
||||||
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
|
|
||||||
|
|
||||||
const SINGLE_PERMISSION_LIST = [
|
|
||||||
{
|
|
||||||
title: "Project",
|
|
||||||
formName: "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Integrations",
|
|
||||||
formName: "integrations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Secret Protect policy",
|
|
||||||
formName: ProjectPermissionSub.SecretApproval
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Roles",
|
|
||||||
formName: "role"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "User Management",
|
|
||||||
formName: "member"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Group Management",
|
|
||||||
formName: "groups"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Machine Identity Management",
|
|
||||||
formName: "identity"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Webhooks",
|
|
||||||
formName: "webhooks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Service Tokens",
|
|
||||||
formName: "service-tokens"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
formName: "settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Environments",
|
|
||||||
formName: "environments"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Tags",
|
|
||||||
formName: "tags"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "IP Allowlist",
|
|
||||||
formName: "ip-allowlist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Certificate Authorities",
|
|
||||||
formName: "certificate-authorities"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Certificates",
|
|
||||||
formName: "certificates"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Certificate Templates",
|
|
||||||
formName: "certificate-templates"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "PKI Collections",
|
|
||||||
formName: "pki-collections"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "PKI Alerts",
|
|
||||||
formName: "pki-alerts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Secret Rollback",
|
|
||||||
formName: "secret-rollback"
|
|
||||||
}
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
roleSlug: string;
|
roleSlug: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RolePermissionsSection = ({ roleSlug }: Props) => {
|
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
const { data: role } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string);
|
const { data: role, isLoading } = useGetProjectRoleBySlug(
|
||||||
|
currentWorkspace?.slug ?? "",
|
||||||
|
roleSlug as string
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<TFormSchema>({
|
||||||
|
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setValue,
|
|
||||||
getValues,
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isDirty, isSubmitting },
|
formState: { isDirty, isSubmitting },
|
||||||
reset
|
reset
|
||||||
} = useForm<TFormSchema>({
|
} = form;
|
||||||
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
|
|
||||||
resolver: zodResolver(formSchema)
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
||||||
|
|
||||||
const onSubmit = async (el: TFormSchema) => {
|
const onSubmit = async (el: TFormSchema) => {
|
||||||
try {
|
try {
|
||||||
if (!projectSlug || !role?.id) return;
|
if (!projectSlug || !role?.id) return;
|
||||||
|
|
||||||
await updateRole({
|
await updateRole({
|
||||||
id: role?.id as string,
|
id: role?.id as string,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
@@ -143,70 +73,77 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => {
|
|||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<FormProvider {...form}>
|
||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Permissions</h3>
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
{isCustomRole && (
|
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center space-x-4">
|
||||||
<Button
|
{isCustomRole && (
|
||||||
colorSchema="primary"
|
<>
|
||||||
type="submit"
|
{isDirty && (
|
||||||
isDisabled={isSubmitting || !isDirty}
|
<Button
|
||||||
isLoading={isSubmitting}
|
className="mr-4 text-mineshaft-300"
|
||||||
>
|
variant="link"
|
||||||
Save
|
isDisabled={isSubmitting}
|
||||||
</Button>
|
isLoading={isSubmitting}
|
||||||
<Button
|
onClick={() => reset()}
|
||||||
className="ml-4 text-mineshaft-300"
|
>
|
||||||
variant="link"
|
Discard
|
||||||
isDisabled={isSubmitting || !isDirty}
|
</Button>
|
||||||
isLoading={isSubmitting}
|
)}
|
||||||
onClick={() => reset()}
|
<div className="flex items-center">
|
||||||
>
|
<Button
|
||||||
Cancel
|
variant="outline_bg"
|
||||||
</Button>
|
type="submit"
|
||||||
|
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.createPolicy.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalTrigger asChild disabled={isDisabled}>
|
||||||
|
<Button
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
className="h-10 rounded-l-none"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
New policy
|
||||||
|
</Button>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
title="New Policy"
|
||||||
|
subTitle="Policies grant additional permissions."
|
||||||
|
>
|
||||||
|
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className="py-4">
|
||||||
<div className="py-4">
|
{!isLoading && <PermissionEmptyState />}
|
||||||
<TableContainer>
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||||
<Table>
|
<GeneralPermissionOptions
|
||||||
<THead>
|
subject={subject}
|
||||||
<Tr>
|
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||||
<Th className="w-5" />
|
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||||
<Th>Resource</Th>
|
key={`project-permission-${subject}`}
|
||||||
<Th>Permission</Th>
|
isDisabled={isDisabled}
|
||||||
</Tr>
|
>
|
||||||
</THead>
|
{subject === ProjectPermissionSub.Secrets ? (
|
||||||
<TBody>
|
<SecretPermissionConditions isDisabled={isDisabled} />
|
||||||
<RowPermissionSecretsRow
|
) : undefined}
|
||||||
title="Secrets"
|
</GeneralPermissionOptions>
|
||||||
formName={ProjectPermissionSub.Secrets}
|
))}
|
||||||
isEditable={isCustomRole}
|
</div>
|
||||||
setValue={setValue}
|
</FormProvider>
|
||||||
getValue={getValues}
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
<RowPermissionSecretFoldersRow
|
|
||||||
isEditable={isCustomRole}
|
|
||||||
setValue={setValue}
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
{SINGLE_PERMISSION_LIST.map((permission) => {
|
|
||||||
return (
|
|
||||||
<RolePermissionRow
|
|
||||||
title={permission.title}
|
|
||||||
formName={permission.formName}
|
|
||||||
control={control}
|
|
||||||
setValue={setValue}
|
|
||||||
key={`project-role-${roleSlug}-permission-${permission.formName}`}
|
|
||||||
isEditable={isCustomRole}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
import { cloneElement } from "react";
|
||||||
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Button, Checkbox, Tag } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
import { TFormSchema, TProjectPermissionObject } from "../ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props<T extends ProjectPermissionSub> = {
|
||||||
|
title: string;
|
||||||
|
subject: T;
|
||||||
|
actions: TProjectPermissionObject[T]["actions"];
|
||||||
|
children?: JSX.Element;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
|
||||||
|
subject,
|
||||||
|
actions,
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
isDisabled
|
||||||
|
}: Props<T>) => {
|
||||||
|
const { control } = useFormContext<TFormSchema>();
|
||||||
|
const items = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: `permissions.${subject}`
|
||||||
|
});
|
||||||
|
const [isOpen, setIsOpen] = useToggle();
|
||||||
|
|
||||||
|
if (!items.fields.length) return <div />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center space-x-8 px-5 py-4 text-sm text-gray-300"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => setIsOpen.toggle()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
setIsOpen.toggle();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={isOpen ? faChevronDown : faChevronRight} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">{title}</div>
|
||||||
|
{items.fields.length > 1 && (
|
||||||
|
<div>
|
||||||
|
<Tag size="xs" className="px-2">
|
||||||
|
{items.fields.length} rules
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isOpen && (
|
||||||
|
<div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6">
|
||||||
|
{items.fields.map((el, rootIndex) => (
|
||||||
|
<div key={el.id} className="bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md">
|
||||||
|
<div className="flex text-gray-300">
|
||||||
|
<div className="w-1/4">Actions</div>
|
||||||
|
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||||
|
{actions.map(({ label, value }) => {
|
||||||
|
if (typeof value !== "string") return undefined;
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
key={`${el.id}-${label}`}
|
||||||
|
name={`permissions.${subject}.${rootIndex}.${value}` as any}
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isChecked={Boolean(field.value)}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children &&
|
||||||
|
cloneElement(children, {
|
||||||
|
position: rootIndex
|
||||||
|
})}
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"mt-4 flex justify-start space-x-4",
|
||||||
|
subject === ProjectPermissionSub.Secrets && "justify-end"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!isDisabled && subject === ProjectPermissionSub.Secrets && (
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
variant="star"
|
||||||
|
size="xs"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={() => {
|
||||||
|
items.insert(rootIndex, [
|
||||||
|
{ read: false, edit: false, create: false, delete: false } as any
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
>
|
||||||
|
Add policy
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isDisabled && (
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
className="mt-2 hover:border-red"
|
||||||
|
onClick={() => items.remove(rootIndex)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
>
|
||||||
|
Remove policy
|
||||||
|
</Button>
|
||||||
|
)}{" "}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
ModalClose,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
|
import {
|
||||||
|
formSchema,
|
||||||
|
PROJECT_PERMISSION_OBJECT,
|
||||||
|
TFormSchema
|
||||||
|
} from "../ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NewPermissionRule = ({ onClose }: Props) => {
|
||||||
|
const rootForm = useFormContext<TFormSchema>();
|
||||||
|
|
||||||
|
const form = useForm<{
|
||||||
|
type: ProjectPermissionSub;
|
||||||
|
permissions: NonNullable<TFormSchema["permissions"]>;
|
||||||
|
}>({
|
||||||
|
resolver: zodResolver(
|
||||||
|
formSchema.pick({ permissions: true }).extend({ type: z.nativeEnum(ProjectPermissionSub) })
|
||||||
|
),
|
||||||
|
defaultValues: {
|
||||||
|
type: ProjectPermissionSub.Secrets
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedSubject = form.watch("type");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="type"
|
||||||
|
defaultValue={ProjectPermissionSub.Secrets}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Subject" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{Object.keys(PROJECT_PERMISSION_OBJECT).map((subject) => (
|
||||||
|
<SelectItem value={subject} key={`permission-create-${subject}`}>
|
||||||
|
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormLabel label="Actions" className="my-2" />
|
||||||
|
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||||
|
{PROJECT_PERMISSION_OBJECT?.[selectedSubject]?.actions?.map(({ label, value }) => (
|
||||||
|
<Controller
|
||||||
|
key={`create-permission-${selectedSubject}-${label}`}
|
||||||
|
name={`permissions.${selectedSubject}.0.${value as any}` as any}
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
id={`new-permissions.${selectedSubject}.0.${String(value)}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex space-x-4">
|
||||||
|
<Button
|
||||||
|
onClick={form.handleSubmit((el) => {
|
||||||
|
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
|
||||||
|
if (rootPolicyValue && selectedSubject === ProjectPermissionSub.Secrets) {
|
||||||
|
rootForm.setValue(
|
||||||
|
`permissions.${el.type}`,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||||
|
[...rootPolicyValue, ...(el?.permissions[el.type] || [])],
|
||||||
|
{ shouldDirty: true, shouldTouch: true }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||||
|
rootForm.setValue(`permissions.${el.type}`, el?.permissions?.[el.type], {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
import { faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
|
import { TFormSchema } from "../ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
position?: number;
|
||||||
|
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) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors }
|
||||||
|
} = useFormContext<TFormSchema>();
|
||||||
|
const items = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: `permissions.secrets.${position}.conditions`
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 border-t border-t-gray-800 bg-mineshaft-800 pt-2">
|
||||||
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
|
{items.fields.map((el, index) => {
|
||||||
|
const lhs = watch(`permissions.secrets.${position}.conditions.${index}.lhs`);
|
||||||
|
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.secrets.${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="w-36">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.secrets.${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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.secrets.${position}.conditions.${index}.rhs`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder={getValueLabel(lhs)} />
|
||||||
|
</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?.secrets?.[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?.secrets?.[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: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
New Condition
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user