mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 05:28:51 +00:00
Merge remote-tracking branch 'origin/main' into feat/enforce-oidc-sso
This commit is contained in:
Vendored
+8
@@ -101,6 +101,9 @@ import {
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityKubernetesAuthsInsert,
|
||||
TIdentityKubernetesAuthsUpdate,
|
||||
TIdentityMetadata,
|
||||
TIdentityMetadataInsert,
|
||||
TIdentityMetadataUpdate,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOidcAuthsInsert,
|
||||
TIdentityOidcAuthsUpdate,
|
||||
@@ -546,6 +549,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityUniversalAuthsInsert,
|
||||
TIdentityUniversalAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityMetadata]: KnexOriginal.CompositeTableType<
|
||||
TIdentityMetadata,
|
||||
TIdentityMetadataInsert,
|
||||
TIdentityMetadataUpdate
|
||||
>;
|
||||
[TableName.IdentityKubernetesAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityKubernetesAuths,
|
||||
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-gcp-auths";
|
||||
export * from "./identity-kubernetes-auths";
|
||||
export * from "./identity-metadata";
|
||||
export * from "./identity-oidc-auths";
|
||||
export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
|
||||
@@ -70,6 +70,8 @@ export enum TableName {
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
|
||||
// used by both identity and users
|
||||
IdentityMetadata = "identity_metadata",
|
||||
ScimToken = "scim_tokens",
|
||||
AccessApprovalPolicy = "access_approval_policies",
|
||||
AccessApprovalPolicyApprover = "access_approval_policies_approvers",
|
||||
|
||||
@@ -3,10 +3,11 @@ import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
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";
|
||||
|
||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -100,6 +100,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
async (req, profile, cb) => {
|
||||
try {
|
||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||
|
||||
const email =
|
||||
profile?.email ??
|
||||
// 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({
|
||||
externalId: profile.nameID,
|
||||
email,
|
||||
@@ -130,7 +139,8 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
lastName: lastName as string,
|
||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||
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 });
|
||||
} catch (error) {
|
||||
|
||||
+6
-12
@@ -34,18 +34,12 @@ export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
||||
|
||||
// TODO(akhilmhdh): move this to more centralized
|
||||
export const UnpackedPermissionSchema = z.object({
|
||||
subject: z.union([z.string().min(1), z.string().array()]).optional(),
|
||||
action: z.union([z.string().min(1), z.string().array()]),
|
||||
conditions: z
|
||||
.object({
|
||||
environment: z.string().optional(),
|
||||
secretPath: z
|
||||
.object({
|
||||
$glob: z.string().min(1)
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
subject: z
|
||||
.union([z.string().min(1), z.string().array()])
|
||||
.transform((el) => (typeof el !== "string" ? el[0] : el))
|
||||
.optional(),
|
||||
action: z.union([z.string().min(1), z.string().array()]).transform((el) => (typeof el === "string" ? [el] : el)),
|
||||
conditions: z.unknown().optional()
|
||||
});
|
||||
|
||||
const unpackPermissions = (permissions: unknown) =>
|
||||
|
||||
@@ -168,8 +168,14 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
})
|
||||
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||
.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(
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("username").withSchema(TableName.Users).as("username"),
|
||||
// groups specific
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("groupMembershipId"),
|
||||
db.ref("createdAt").withSchema(TableName.GroupProjectMembership).as("groupMembershipCreatedAt"),
|
||||
@@ -257,6 +263,9 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.withSchema(TableName.ProjectUserAdditionalPrivilege)
|
||||
.as("userAdditionalPrivilegesTemporaryAccessEndTime"),
|
||||
// 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("orgId").withSchema(TableName.Project),
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||
@@ -267,6 +276,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
key: "projectId",
|
||||
parentMapper: ({
|
||||
orgId,
|
||||
username,
|
||||
orgAuthEnforced,
|
||||
membershipId,
|
||||
groupMembershipId,
|
||||
@@ -279,6 +289,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
orgAuthEnforced,
|
||||
userId,
|
||||
projectId,
|
||||
username,
|
||||
id: membershipId || groupMembershipId,
|
||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||
@@ -354,6 +365,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
temporaryAccessEndTime: userAdditionalPrivilegesTemporaryAccessEndTime,
|
||||
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.IdentityProjectMembership}.id`
|
||||
)
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityProjectMembership}.identityId`)
|
||||
.leftJoin(
|
||||
TableName.ProjectRoles,
|
||||
`${TableName.IdentityProjectMembershipRole}.customRoleId`,
|
||||
@@ -415,11 +436,17 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembership}.projectId`,
|
||||
`${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)
|
||||
.select(selectAllTableCols(TableName.IdentityProjectMembershipRole))
|
||||
.select(
|
||||
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("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||
@@ -443,15 +470,19 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
db
|
||||
.ref("temporaryAccessEndTime")
|
||||
.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({
|
||||
data: docs,
|
||||
key: "membershipId",
|
||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId }) => ({
|
||||
parentMapper: ({ membershipId, membershipCreatedAt, membershipUpdatedAt, orgId, identityName }) => ({
|
||||
id: membershipId,
|
||||
identityId,
|
||||
username: identityName,
|
||||
projectId,
|
||||
createdAt: membershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt,
|
||||
@@ -489,6 +520,15 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
temporaryAccessStartTime: identityApTemporaryAccessStartTime,
|
||||
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 { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import handlebars from "handlebars";
|
||||
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
} from "@app/db/schemas";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { objectify } from "@app/lib/fn";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TOrgRoleDALFactory } from "@app/services/org/org-role-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 { TPermissionDALFactory } from "./permission-dal";
|
||||
import { validateOrgSSO } from "./permission-fns";
|
||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-types";
|
||||
import { TBuildOrgPermissionDTO, TBuildProjectPermissionDTO } from "./permission-service-types";
|
||||
import {
|
||||
buildServiceTokenProjectPermission,
|
||||
projectAdminPermissions,
|
||||
@@ -72,7 +74,7 @@ export const permissionServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||
const rules = projectUserRoles
|
||||
.map(({ role, permissions }) => {
|
||||
switch (role) {
|
||||
@@ -98,9 +100,7 @@ export const permissionServiceFactory = ({
|
||||
})
|
||||
.reduce((curr, prev) => prev.concat(curr), []);
|
||||
|
||||
return createMongoAbility<ProjectPermissionSet>(rules, {
|
||||
conditionsMatcher
|
||||
});
|
||||
return rules;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -223,8 +223,32 @@ export const permissionServiceFactory = ({
|
||||
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 {
|
||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
||||
permission,
|
||||
membership: userProjectPermission,
|
||||
hasRole: (role: string) =>
|
||||
userProjectPermission.roles.findIndex(
|
||||
@@ -262,8 +286,32 @@ export const permissionServiceFactory = ({
|
||||
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 {
|
||||
permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)),
|
||||
permission,
|
||||
membership: identityProjectPermission,
|
||||
hasRole: (role: string) =>
|
||||
identityProjectPermission.roles.findIndex(
|
||||
@@ -346,14 +394,22 @@ export const permissionServiceFactory = ({
|
||||
if (isCustomRole) {
|
||||
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||
if (!projectRole) throw new NotFoundError({ message: `Specified role was not found: ${role}` });
|
||||
const rules = buildProjectPermissionRules([
|
||||
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||
]);
|
||||
return {
|
||||
permission: buildProjectPermission([
|
||||
{ role: ProjectMembershipRole.Custom, permissions: projectRole.permissions }
|
||||
]),
|
||||
permission: createMongoAbility<ProjectPermissionSet>(rules, {
|
||||
conditionsMatcher
|
||||
}),
|
||||
role: projectRole
|
||||
};
|
||||
}
|
||||
return { permission: buildProjectPermission([{ role, permissions: [] }]) };
|
||||
|
||||
const rules = buildProjectPermissionRules([{ role, permissions: [] }]);
|
||||
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
|
||||
conditionsMatcher
|
||||
});
|
||||
return { permission };
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -364,6 +420,6 @@ export const permissionServiceFactory = ({
|
||||
getOrgPermissionByRole,
|
||||
getProjectPermissionByRole,
|
||||
buildOrgPermission,
|
||||
buildProjectPermission
|
||||
buildProjectPermissionRules
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,47 @@
|
||||
export type TBuildProjectPermissionDTO = {
|
||||
permissions?: unknown;
|
||||
role: string;
|
||||
}[];
|
||||
import picomatch from "picomatch";
|
||||
import { z } from "zod";
|
||||
|
||||
export type TBuildOrgPermissionDTO = {
|
||||
permissions?: unknown;
|
||||
role: string;
|
||||
}[];
|
||||
export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$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 { z } from "zod";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
|
||||
|
||||
export enum ProjectPermissionActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@@ -37,7 +41,25 @@ export enum ProjectPermissionSub {
|
||||
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;
|
||||
secretPath: string;
|
||||
};
|
||||
@@ -45,11 +67,14 @@ type SubjectFields = {
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
||||
(
|
||||
| ProjectPermissionSub.SecretFolders
|
||||
| (ForcedSubject<ProjectPermissionSub.SecretFolders> & SecretFolderSubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
@@ -76,128 +101,230 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||
|
||||
export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermissionSub][] = [
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Secrets],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Secrets],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets],
|
||||
const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
|
||||
z
|
||||
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
|
||||
.transform((el) => (typeof el === "string" ? [el] : el));
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval],
|
||||
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
|
||||
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation],
|
||||
const SecretConditionSchema = z
|
||||
.object({
|
||||
environment: z.union([
|
||||
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],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Member],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Member],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Member],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Member],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Groups],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Groups],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Groups],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Groups],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Role],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Role],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Role],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Role],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Integrations],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Integrations],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Identity],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Identity],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Identity],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Identity],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Settings],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Settings],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Settings],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Settings],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Environments],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Environments],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Environments],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Environments],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
|
||||
|
||||
// TODO(Daniel): Remove the audit logs permissions from project-level permissions.
|
||||
// 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.
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList],
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.Certificates],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.Certificates],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts],
|
||||
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections],
|
||||
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Project],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Project],
|
||||
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
||||
];
|
||||
export const ProjectPermissionSchema = z.discriminatedUnion("subject", [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: SecretConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretApproval).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.SecretRotation).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.SecretRollback).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_ENUM([ProjectPermissionActions.Read, ProjectPermissionActions.Create]).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Member).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.Groups).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.Role).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.Integrations).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.Webhooks).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.Identity).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.ServiceTokens).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.Settings).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.Environments).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.Tags).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.AuditLogs).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.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 { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
// Admins get full access to everything
|
||||
fullProjectPermissionSet.forEach((permission) => {
|
||||
const [action, subject] = permission;
|
||||
can(action, subject);
|
||||
[
|
||||
ProjectPermissionSub.Secrets,
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -206,73 +333,116 @@ export const projectAdminPermissions = buildAdminPermissionRules();
|
||||
const buildMemberPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Settings
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
|
||||
|
||||
// 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(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
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.PkiCollections);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||
|
||||
return rules;
|
||||
};
|
||||
@@ -382,32 +552,19 @@ export const isAtLeastAsPrivilegedWorkspace = (
|
||||
|
||||
return set1.size >= set2.size;
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
/*
|
||||
* Case: The user requests to create a role with permissions that are not valid and not supposed to be used ever.
|
||||
* 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.
|
||||
* If the permission set contains invalid permissions, the size will be different, and result in incorrect results.
|
||||
*/
|
||||
export const validateProjectPermissions = (permissions: unknown) => {
|
||||
const parsedPermissions =
|
||||
typeof permissions === "string" ? (JSON.parse(permissions) as string[]) : (permissions as string[]);
|
||||
|
||||
const flattenedPermissions = [...parsedPermissions];
|
||||
|
||||
for (const perm of flattenedPermissions) {
|
||||
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"
|
||||
});
|
||||
}
|
||||
export const SecretV2SubjectFieldMapper = (arg: string) => {
|
||||
switch (arg) {
|
||||
case "environment":
|
||||
return null;
|
||||
case "secretPath":
|
||||
return null;
|
||||
case "secretName":
|
||||
return `${TableName.SecretV2}.key`;
|
||||
case "secretTags":
|
||||
return `${TableName.SecretTag}.slug`;
|
||||
default:
|
||||
throw new BadRequestError({ message: `Invalid dynamic knex operator field: ${arg}` });
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
@@ -23,6 +23,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
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 { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
@@ -51,6 +52,8 @@ type TSamlConfigServiceFactoryDep = {
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||
>;
|
||||
|
||||
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
@@ -71,7 +74,8 @@ export const samlConfigServiceFactory = ({
|
||||
permissionService,
|
||||
licenseService,
|
||||
tokenService,
|
||||
smtpService
|
||||
smtpService,
|
||||
identityMetadataDAL
|
||||
}: TSamlConfigServiceFactoryDep) => {
|
||||
const createSamlCfg = async ({
|
||||
cert,
|
||||
@@ -332,7 +336,8 @@ export const samlConfigServiceFactory = ({
|
||||
lastName,
|
||||
authProvider,
|
||||
orgId,
|
||||
relayState
|
||||
relayState,
|
||||
metadata
|
||||
}: TSamlLoginDTO) => {
|
||||
const appCfg = getConfig();
|
||||
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;
|
||||
});
|
||||
} 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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,4 +53,5 @@ export type TSamlLoginDTO = {
|
||||
orgId: string;
|
||||
// saml thingy
|
||||
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.",
|
||||
membershipId: "The ID of the membership to update.",
|
||||
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: {
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
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 { identityServiceFactory } from "@app/services/identity/identity-service";
|
||||
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 identityDAL = identityDALFactory(db);
|
||||
const identityMetadataDAL = identityMetadataDALFactory(db);
|
||||
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
|
||||
const identityOrgMembershipDAL = identityOrgDALFactory(db);
|
||||
const identityProjectDAL = identityProjectDALFactory(db);
|
||||
@@ -386,6 +388,7 @@ export const registerRoutes = async (
|
||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
|
||||
|
||||
const samlService = samlConfigServiceFactory({
|
||||
identityMetadataDAL,
|
||||
permissionService,
|
||||
orgBotDAL,
|
||||
orgDAL,
|
||||
@@ -489,6 +492,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
const orgService = orgServiceFactory({
|
||||
userAliasDAL,
|
||||
identityMetadataDAL,
|
||||
licenseService,
|
||||
samlConfigDAL,
|
||||
orgRoleDAL,
|
||||
@@ -1028,7 +1032,8 @@ export const registerRoutes = async (
|
||||
identityDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityProjectDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
identityMetadataDAL
|
||||
});
|
||||
|
||||
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
|
||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||
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
|
||||
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
|
||||
actorId: req.permission.id,
|
||||
|
||||
@@ -29,7 +29,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(IDENTITIES.CREATE.name),
|
||||
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: {
|
||||
200: z.object({
|
||||
@@ -93,7 +97,11 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
body: z.object({
|
||||
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: {
|
||||
200: z.object({
|
||||
@@ -193,6 +201,14 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
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({
|
||||
id: true,
|
||||
name: true,
|
||||
|
||||
@@ -130,18 +130,24 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
membership: OrgMembershipsSchema.merge(
|
||||
z.object({
|
||||
user: UsersSchema.pick({
|
||||
username: true,
|
||||
email: true,
|
||||
isEmailVerified: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
}).merge(z.object({ publicKey: z.string().nullable() }))
|
||||
})
|
||||
).omit({ createdAt: true, updatedAt: true })
|
||||
membership: OrgMembershipsSchema.extend({
|
||||
metadata: z
|
||||
.object({
|
||||
key: z.string().trim().min(1),
|
||||
id: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1)
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
user: UsersSchema.pick({
|
||||
username: 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({
|
||||
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: {
|
||||
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 { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
||||
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 { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
||||
|
||||
@@ -42,10 +42,25 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
tx?: Knex
|
||||
) => {
|
||||
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)
|
||||
.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.IdentityMetadata, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.IdentityOrgMembership}.identityId`, `${TableName.IdentityMetadata}.identityId`)
|
||||
.andOn(`${TableName.IdentityOrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||
})
|
||||
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
||||
// cr stands for custom role
|
||||
.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("id").as("identityId").withSchema(TableName.Identity))
|
||||
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
|
||||
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity));
|
||||
|
||||
if (limit) {
|
||||
void query.offset(offset).limit(limit);
|
||||
}
|
||||
.select(
|
||||
db.ref("name").as("identityName").withSchema(TableName.Identity),
|
||||
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity)
|
||||
)
|
||||
.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) {
|
||||
switch (orderBy) {
|
||||
@@ -80,9 +98,10 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
|
||||
return docs.map(
|
||||
({
|
||||
const formattedDocs = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: ({
|
||||
crId,
|
||||
crDescription,
|
||||
crSlug,
|
||||
@@ -91,16 +110,21 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
identityId,
|
||||
identityName,
|
||||
identityAuthMethod,
|
||||
...el
|
||||
role,
|
||||
roleId,
|
||||
id,
|
||||
orgId,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}) => ({
|
||||
...el,
|
||||
role,
|
||||
roleId,
|
||||
identityId,
|
||||
identity: {
|
||||
id: identityId,
|
||||
name: identityName,
|
||||
authMethod: identityAuthMethod
|
||||
},
|
||||
customRole: el.roleId
|
||||
id,
|
||||
orgId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
customRole: roleId
|
||||
? {
|
||||
id: crId,
|
||||
name: crName,
|
||||
@@ -108,9 +132,27 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
permissions: crPermission,
|
||||
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) {
|
||||
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 { TIdentityDALFactory } from "./identity-dal";
|
||||
import { TIdentityMetadataDALFactory } from "./identity-metadata-dal";
|
||||
import { TIdentityOrgDALFactory } from "./identity-org-dal";
|
||||
import {
|
||||
TCreateIdentityDTO,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
|
||||
type TIdentityServiceFactoryDep = {
|
||||
identityDAL: TIdentityDALFactory;
|
||||
identityMetadataDAL: TIdentityMetadataDALFactory;
|
||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
@@ -32,6 +34,7 @@ export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
||||
|
||||
export const identityServiceFactory = ({
|
||||
identityDAL,
|
||||
identityMetadataDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityProjectDAL,
|
||||
permissionService,
|
||||
@@ -44,7 +47,8 @@ export const identityServiceFactory = ({
|
||||
orgId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
metadata
|
||||
}: TCreateIdentityDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
@@ -78,6 +82,17 @@ export const identityServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (metadata && metadata.length) {
|
||||
await identityMetadataDAL.insertMany(
|
||||
metadata.map(({ key, value }) => ({
|
||||
identityId: newIdentity.id,
|
||||
orgId,
|
||||
key,
|
||||
value
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
return newIdentity;
|
||||
});
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
@@ -92,7 +107,8 @@ export const identityServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
metadata
|
||||
}: TUpdateIdentityDTO) => {
|
||||
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: 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 newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
|
||||
if (role) {
|
||||
await identityOrgMembershipDAL.update(
|
||||
{ identityId: id },
|
||||
await identityOrgMembershipDAL.updateById(
|
||||
identityOrgMembership.id,
|
||||
{
|
||||
role: customRole ? OrgMembershipRole.Custom : role,
|
||||
roleId: customRole?.id || null
|
||||
@@ -143,6 +159,20 @@ export const identityServiceFactory = ({
|
||||
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;
|
||||
});
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
||||
export type TCreateIdentityDTO = {
|
||||
role: string;
|
||||
name: string;
|
||||
metadata?: { key: string; value: string }[];
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TUpdateIdentityDTO = {
|
||||
id: string;
|
||||
role?: string;
|
||||
name?: string;
|
||||
metadata?: { key: string; value: string }[];
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TDeleteIdentityDTO = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TUserEncryptionKeys } from "@app/db/schemas";
|
||||
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>;
|
||||
|
||||
@@ -19,6 +19,11 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
|
||||
`${TableName.UserEncryptionKey}.userId`,
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.OrgMembership}.userId`, `${TableName.IdentityMetadata}.userId`)
|
||||
.andOn(`${TableName.OrgMembership}.orgId`, `${TableName.IdentityMetadata}.orgId`);
|
||||
})
|
||||
.select(
|
||||
db.ref("id").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("isEmailVerified").withSchema(TableName.Users),
|
||||
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
|
||||
.first();
|
||||
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER
|
||||
|
||||
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 {
|
||||
...data,
|
||||
user: { email, isEmailVerified, username, firstName, lastName, id: userId, publicKey }
|
||||
};
|
||||
return doc?.[0];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find org membership by id" });
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
@@ -73,12 +74,13 @@ type TOrgServiceFactoryDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
groupDAL: TGroupDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
identityMetadataDAL: Pick<TIdentityMetadataDALFactory, "delete" | "insertMany" | "transaction">;
|
||||
projectMembershipDAL: Pick<
|
||||
TProjectMembershipDALFactory,
|
||||
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne" | "findById">;
|
||||
incidentContactDAL: TIncidentContactsDALFactory;
|
||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "findEnforceableOidcCfg">;
|
||||
@@ -118,7 +120,8 @@ export const orgServiceFactory = ({
|
||||
samlConfigDAL,
|
||||
oidcConfigDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
projectUserMembershipRoleDAL,
|
||||
identityMetadataDAL
|
||||
}: TOrgServiceFactoryDep) => {
|
||||
/*
|
||||
* Get organization details by the organization id
|
||||
@@ -408,20 +411,22 @@ export const orgServiceFactory = ({
|
||||
userId,
|
||||
membershipId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
metadata
|
||||
}: TUpdateOrgMembershipDTO) => {
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
|
||||
|
||||
const foundMembership = await orgMembershipDAL.findOne({
|
||||
id: membershipId,
|
||||
orgId
|
||||
});
|
||||
const foundMembership = await orgMembershipDAL.findById(membershipId);
|
||||
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)
|
||||
throw new UnauthorizedError({ message: "Cannot update own organization membership" });
|
||||
|
||||
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
||||
let userRole = role;
|
||||
let userRoleId: string | null = null;
|
||||
if (role && isCustomRole) {
|
||||
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
||||
if (!customRole) throw new BadRequestError({ name: "UpdateMembership", message: "Organization role not found" });
|
||||
@@ -432,17 +437,31 @@ export const orgServiceFactory = ({
|
||||
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||
});
|
||||
|
||||
const [membership] = await orgDAL.updateMembership(
|
||||
{ id: membershipId, orgId },
|
||||
{
|
||||
role: OrgMembershipRole.Custom,
|
||||
roleId: customRole.id
|
||||
}
|
||||
);
|
||||
return membership;
|
||||
userRole = OrgMembershipRole.Custom;
|
||||
userRoleId = customRole.id;
|
||||
}
|
||||
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;
|
||||
};
|
||||
/*
|
||||
|
||||
@@ -9,6 +9,7 @@ export type TUpdateOrgMembershipDTO = {
|
||||
role?: string;
|
||||
isActive?: boolean;
|
||||
actorOrgId: string | undefined;
|
||||
metadata?: { key: string; value: string }[];
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub,
|
||||
validateProjectPermissions
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
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" });
|
||||
}
|
||||
|
||||
validateProjectPermissions(data.permissions);
|
||||
|
||||
const role = await projectRoleDAL.create({
|
||||
...data,
|
||||
projectId
|
||||
@@ -127,10 +124,6 @@ export const projectRoleServiceFactory = ({
|
||||
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(
|
||||
{ id: roleId, projectId },
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user