mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 11:29:42 +00:00
feature: scim group org role mapping
This commit is contained in:
Vendored
+2
@@ -39,6 +39,7 @@ import { TCertificateServiceFactory } from "@app/services/certificate/certificat
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
@@ -185,6 +186,7 @@ declare module "fastify" {
|
||||
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
||||
cmek: TCmekServiceFactory;
|
||||
migration: TExternalMigrationServiceFactory;
|
||||
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
Vendored
+10
@@ -336,6 +336,11 @@ import {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TExternalGroupOrgRoleMappings,
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
TExternalGroupOrgRoleMappingsUpdate
|
||||
} from "@app/db/schemas/external-group-org-role-mappings";
|
||||
import {
|
||||
TSecretV2TagJunction,
|
||||
TSecretV2TagJunctionInsert,
|
||||
@@ -808,5 +813,10 @@ declare module "knex/types/tables" {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
>;
|
||||
[TableName.ExternalGroupOrgRoleMapping]: KnexOriginal.CompositeTableType<
|
||||
TExternalGroupOrgRoleMappings,
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
TExternalGroupOrgRoleMappingsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// add external group to org role mapping table
|
||||
if (!(await knex.schema.hasTable(TableName.ExternalGroupOrgRoleMapping))) {
|
||||
await knex.schema.createTable(TableName.ExternalGroupOrgRoleMapping, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("groupName").notNullable();
|
||||
t.index("groupName");
|
||||
t.string("role").notNullable();
|
||||
t.uuid("roleId");
|
||||
t.foreign("roleId").references("id").inTable(TableName.OrgRoles);
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
t.unique(["orgId", "groupName"]);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.ExternalGroupOrgRoleMapping);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.ExternalGroupOrgRoleMapping)) {
|
||||
await dropOnUpdateTrigger(knex, TableName.ExternalGroupOrgRoleMapping);
|
||||
|
||||
await knex.schema.dropTable(TableName.ExternalGroupOrgRoleMapping);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 ExternalGroupOrgRoleMappingsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
groupName: z.string(),
|
||||
role: z.string(),
|
||||
roleId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TExternalGroupOrgRoleMappings = z.infer<typeof ExternalGroupOrgRoleMappingsSchema>;
|
||||
export type TExternalGroupOrgRoleMappingsInsert = Omit<
|
||||
z.input<typeof ExternalGroupOrgRoleMappingsSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TExternalGroupOrgRoleMappingsUpdate = Partial<
|
||||
Omit<z.input<typeof ExternalGroupOrgRoleMappingsSchema>, TImmutableDBKeys>
|
||||
>;
|
||||
@@ -17,6 +17,7 @@ export enum TableName {
|
||||
Groups = "groups",
|
||||
GroupProjectMembership = "group_project_memberships",
|
||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||
ExternalGroupOrgRoleMapping = "external_group_org_role_mappings",
|
||||
UserGroupMembership = "user_group_membership",
|
||||
UserAliases = "user_aliases",
|
||||
UserEncryptionKey = "user_encryption_keys",
|
||||
|
||||
@@ -190,7 +190,9 @@ export enum EventType {
|
||||
DELETE_CMEK = "delete-cmek",
|
||||
GET_CMEKS = "get-cmeks",
|
||||
CMEK_ENCRYPT = "cmek-encrypt",
|
||||
CMEK_DECRYPT = "cmek-decrypt"
|
||||
CMEK_DECRYPT = "cmek-decrypt",
|
||||
UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "update-external-group-org-role-mapping",
|
||||
GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS = "get-external-group-org-role-mapping"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@@ -1604,6 +1606,18 @@ interface CmekDecryptEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetExternalGroupOrgRoleMappingsEvent {
|
||||
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
||||
metadata?: Record<string, never>; // not needed, based off orgId
|
||||
}
|
||||
|
||||
interface UpdateExternalGroupOrgRoleMappingsEvent {
|
||||
type: EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS;
|
||||
metadata: {
|
||||
mappings: { groupName: string; roleSlug: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -1750,4 +1764,6 @@ export type Event =
|
||||
| DeleteCmekEvent
|
||||
| GetCmeksEvent
|
||||
| CmekEncryptEvent
|
||||
| CmekDecryptEvent;
|
||||
| CmekDecryptEvent
|
||||
| GetExternalGroupOrgRoleMappingsEvent
|
||||
| UpdateExternalGroupOrgRoleMappingsEvent;
|
||||
|
||||
@@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { scimPatch } from "scim-patch";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
@@ -13,6 +13,7 @@ import { BadRequestError, NotFoundError, ScimRequestError, UnauthorizedError } f
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
|
||||
@@ -71,7 +72,10 @@ type TScimServiceFactoryDep = {
|
||||
| "transaction"
|
||||
| "updateMembershipById"
|
||||
>;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
|
||||
orgMembershipDAL: Pick<
|
||||
TOrgMembershipDALFactory,
|
||||
"find" | "findOne" | "create" | "updateById" | "findById" | "update"
|
||||
>;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
||||
groupDAL: Pick<
|
||||
@@ -102,6 +106,7 @@ type TScimServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
|
||||
};
|
||||
|
||||
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
||||
@@ -122,7 +127,8 @@ export const scimServiceFactory = ({
|
||||
projectBotDAL,
|
||||
permissionService,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
smtpService
|
||||
smtpService,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
}: TScimServiceFactoryDep) => {
|
||||
const createScimToken = async ({
|
||||
actor,
|
||||
@@ -692,6 +698,43 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const $syncNewMembersRoles = async (group: TGroups, members: TScimGroup["members"]) => {
|
||||
// this function handles configuring newly provisioned users org membership if an external group mapping exists
|
||||
|
||||
if (!members.length) return;
|
||||
|
||||
const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({
|
||||
orgId: group.orgId,
|
||||
groupName: group.name
|
||||
});
|
||||
|
||||
// no mapping, user will have default org membership
|
||||
if (!externalGroupMapping) return;
|
||||
|
||||
// only get org memberships that are new (invites)
|
||||
const newOrgMemberships = await orgMembershipDAL.find({
|
||||
status: "invited",
|
||||
$in: {
|
||||
id: members.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
|
||||
if (!newOrgMemberships.length) return;
|
||||
|
||||
// set new membership roles to group mapping value
|
||||
await orgMembershipDAL.update(
|
||||
{
|
||||
$in: {
|
||||
id: newOrgMemberships.map((membership) => membership.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
role: externalGroupMapping.role,
|
||||
roleId: externalGroupMapping.roleId
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const createScimGroup = async ({ displayName, orgId, members }: TCreateScimGroupDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
@@ -745,6 +788,8 @@ export const scimServiceFactory = ({
|
||||
tx
|
||||
});
|
||||
|
||||
await $syncNewMembersRoles(group, members);
|
||||
|
||||
return { group, newMembers };
|
||||
}
|
||||
|
||||
@@ -820,22 +865,41 @@ export const scimServiceFactory = ({
|
||||
orgId: string,
|
||||
{ displayName, members = [] }: { displayName: string; members: { value: string }[] }
|
||||
) => {
|
||||
const updatedGroup = await groupDAL.transaction(async (tx) => {
|
||||
const [group] = await groupDAL.update(
|
||||
{
|
||||
id: groupId,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
name: displayName
|
||||
}
|
||||
);
|
||||
let group = await groupDAL.findOne({
|
||||
id: groupId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
const updatedGroup = await groupDAL.transaction(async (tx) => {
|
||||
if (group.name !== displayName) {
|
||||
await externalGroupOrgRoleMappingDAL.update(
|
||||
{
|
||||
groupName: group.name,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
groupName: displayName
|
||||
}
|
||||
);
|
||||
|
||||
const [modifiedGroup] = await groupDAL.update(
|
||||
{
|
||||
id: groupId,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
name: displayName
|
||||
}
|
||||
);
|
||||
|
||||
group = modifiedGroup;
|
||||
}
|
||||
|
||||
const orgMemberships = members.length
|
||||
@@ -892,6 +956,8 @@ export const scimServiceFactory = ({
|
||||
return group;
|
||||
});
|
||||
|
||||
await $syncNewMembersRoles(group, members);
|
||||
|
||||
return updatedGroup;
|
||||
};
|
||||
|
||||
|
||||
@@ -97,6 +97,8 @@ import { certificateTemplateDALFactory } from "@app/services/certificate-templat
|
||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||
import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
|
||||
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
|
||||
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
@@ -336,6 +338,8 @@ export const registerRoutes = async (
|
||||
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
||||
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
|
||||
|
||||
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@@ -442,7 +446,8 @@ export const registerRoutes = async (
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
permissionService,
|
||||
smtpService
|
||||
smtpService,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
|
||||
const ldapService = ldapConfigServiceFactory({
|
||||
@@ -537,7 +542,12 @@ export const registerRoutes = async (
|
||||
orgService,
|
||||
licenseService
|
||||
});
|
||||
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL, orgDAL });
|
||||
const orgRoleService = orgRoleServiceFactory({
|
||||
permissionService,
|
||||
orgRoleDAL,
|
||||
orgDAL,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
const superAdminService = superAdminServiceFactory({
|
||||
userDAL,
|
||||
authService: loginService,
|
||||
@@ -1231,6 +1241,13 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({
|
||||
permissionService,
|
||||
licenseService,
|
||||
orgRoleDAL,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
|
||||
await superAdminService.initServerCfg();
|
||||
//
|
||||
// setup the communication with license key server
|
||||
@@ -1316,7 +1333,8 @@ export const registerRoutes = async (
|
||||
orgAdmin: orgAdminService,
|
||||
slack: slackService,
|
||||
workflowIntegration: workflowIntegrationService,
|
||||
migration: migrationService
|
||||
migration: migrationService,
|
||||
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ExternalGroupOrgRoleMappingsSchema } from "@app/db/schemas/external-group-org-role-mappings";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerExternalGroupOrgRoleMappingRouter = async (server: FastifyZodProvider) => {
|
||||
// get mappings for current org
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: ExternalGroupOrgRoleMappingsSchema.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const mappings = server.services.externalGroupOrgRoleMapping.listExternalGroupOrgRoleMappings(req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
orgId: req.permission.orgId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS
|
||||
}
|
||||
});
|
||||
|
||||
return mappings;
|
||||
}
|
||||
});
|
||||
|
||||
// update mappings for current org
|
||||
server.route({
|
||||
method: "POST", // using post since this endpoint creates, updates and deletes mappings
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
mappings: z
|
||||
.object({
|
||||
groupName: z.string().trim().min(1),
|
||||
roleSlug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.toLowerCase()
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Role must be a valid slug"
|
||||
})
|
||||
})
|
||||
.array()
|
||||
}),
|
||||
response: {
|
||||
200: ExternalGroupOrgRoleMappingsSchema.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { body, permission } = req;
|
||||
|
||||
const mappings = server.services.externalGroupOrgRoleMapping.updateExternalGroupOrgRoleMappings(body, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
orgId: permission.orgId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.UPDATE_EXTERNAL_GROUP_ORG_ROLE_MAPPINGS,
|
||||
metadata: body
|
||||
}
|
||||
});
|
||||
|
||||
return mappings;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { registerProjectBotRouter } from "./bot-router";
|
||||
import { registerCaRouter } from "./certificate-authority-router";
|
||||
import { registerCertRouter } from "./certificate-router";
|
||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
|
||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
||||
@@ -106,4 +107,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||
await server.register(registerCmekRouter, { prefix: "/kms" });
|
||||
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
||||
};
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { Tables } from "knex/types/tables";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TExternalGroupOrgRoleMappings } from "@app/db/schemas/external-group-org-role-mappings";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TExternalGroupOrgRoleMappingDALFactory = ReturnType<typeof externalGroupOrgRoleMappingDALFactory>;
|
||||
|
||||
export const externalGroupOrgRoleMappingDALFactory = (db: TDbClient) => {
|
||||
const externalGroupOrgRoleMappingOrm = ormify(db, TableName.ExternalGroupOrgRoleMapping);
|
||||
|
||||
const updateExternalGroupOrgRoleMappingForOrg = async (
|
||||
orgId: string,
|
||||
newMappings: readonly Tables[TableName.ExternalGroupOrgRoleMapping]["insert"][]
|
||||
) => {
|
||||
const currentMappings = await externalGroupOrgRoleMappingOrm.find({ orgId });
|
||||
|
||||
const newMap = new Map(newMappings.map((mapping) => [mapping.groupName, mapping]));
|
||||
const currentMap = new Map(currentMappings.map((mapping) => [mapping.groupName, mapping]));
|
||||
|
||||
const mappingsToDelete = currentMappings.filter((mapping) => !newMap.has(mapping.groupName));
|
||||
const mappingsToUpdate = currentMappings
|
||||
.filter((mapping) => newMap.has(mapping.groupName))
|
||||
.map((mapping) => ({ id: mapping.id, ...newMap.get(mapping.groupName) }));
|
||||
const mappingsToInsert = newMappings.filter((mapping) => !currentMap.has(mapping.groupName));
|
||||
|
||||
const mappings = await externalGroupOrgRoleMappingOrm.transaction(async (tx) => {
|
||||
await externalGroupOrgRoleMappingOrm.delete({ $in: { id: mappingsToDelete.map((mapping) => mapping.id) } }, tx);
|
||||
|
||||
const updatedMappings: TExternalGroupOrgRoleMappings[] = [];
|
||||
for await (const { id, ...mappingData } of mappingsToUpdate) {
|
||||
const updatedMapping = await externalGroupOrgRoleMappingOrm.update({ id }, mappingData, tx);
|
||||
updatedMappings.push(updatedMapping[0]);
|
||||
}
|
||||
|
||||
const insertedMappings = await externalGroupOrgRoleMappingOrm.insertMany(mappingsToInsert, tx);
|
||||
|
||||
return [...updatedMappings, ...insertedMappings];
|
||||
});
|
||||
|
||||
return mappings;
|
||||
};
|
||||
|
||||
return { ...externalGroupOrgRoleMappingOrm, updateExternalGroupOrgRoleMappingForOrg };
|
||||
};
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||
import { isCustomOrgRole } from "@app/services/org/org-role-fns";
|
||||
|
||||
import { TExternalGroupOrgMembershipRoleMappingDTO } from "./external-group-org-role-mapping-types";
|
||||
|
||||
export const constructGroupOrgMembershipRoleMappings = async ({
|
||||
mappingsDTO,
|
||||
orgId,
|
||||
orgRoleDAL,
|
||||
licenseService
|
||||
}: {
|
||||
mappingsDTO: TExternalGroupOrgMembershipRoleMappingDTO[];
|
||||
orgRoleDAL: TOrgRoleDALFactory;
|
||||
licenseService: TLicenseServiceFactory;
|
||||
orgId: string;
|
||||
}) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
|
||||
// prevent setting custom values if not in plan
|
||||
if (mappingsDTO.some((map) => isCustomOrgRole(map.roleSlug)) && !plan?.rbac)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to set group organization role mapping due to plan RBAC restriction. Upgrade plan to set custom role mapping."
|
||||
});
|
||||
|
||||
const customRoleSlugs = mappingsDTO
|
||||
.filter((mapping) => isCustomOrgRole(mapping.roleSlug))
|
||||
.map((mapping) => mapping.roleSlug);
|
||||
|
||||
let customRolesMap: Map<string, TOrgRoles> = new Map();
|
||||
if (customRoleSlugs.length > 0) {
|
||||
const customRoles = await orgRoleDAL.find({
|
||||
$in: {
|
||||
slug: customRoleSlugs
|
||||
}
|
||||
});
|
||||
|
||||
customRolesMap = new Map(customRoles.map((role) => [role.slug, role]));
|
||||
}
|
||||
|
||||
const mappings = mappingsDTO.map(({ roleSlug, groupName }) => {
|
||||
if (isCustomOrgRole(roleSlug)) {
|
||||
const customRole = customRolesMap.get(roleSlug);
|
||||
|
||||
if (!customRole) throw new NotFoundError({ message: `Custom role ${roleSlug} not found.` });
|
||||
|
||||
return {
|
||||
groupName,
|
||||
role: OrgMembershipRole.Custom,
|
||||
roleId: customRole.id,
|
||||
orgId
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
groupName,
|
||||
role: roleSlug,
|
||||
roleId: null, // need to set explicitly null for updates
|
||||
orgId
|
||||
};
|
||||
});
|
||||
|
||||
return mappings;
|
||||
};
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { constructGroupOrgMembershipRoleMappings } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-fns";
|
||||
import { TSyncExternalGroupOrgMembershipRoleMappingsDTO } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-types";
|
||||
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||
|
||||
import { TExternalGroupOrgRoleMappingDALFactory } from "./external-group-org-role-mapping-dal";
|
||||
|
||||
type TExternalGroupOrgRoleMappingServiceFactoryDep = {
|
||||
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
licenseService: TLicenseServiceFactory;
|
||||
orgRoleDAL: TOrgRoleDALFactory;
|
||||
};
|
||||
|
||||
export type TExternalGroupOrgRoleMappingServiceFactory = ReturnType<typeof externalGroupOrgRoleMappingServiceFactory>;
|
||||
|
||||
export const externalGroupOrgRoleMappingServiceFactory = ({
|
||||
externalGroupOrgRoleMappingDAL,
|
||||
licenseService,
|
||||
permissionService,
|
||||
orgRoleDAL
|
||||
}: TExternalGroupOrgRoleMappingServiceFactoryDep) => {
|
||||
const listExternalGroupOrgRoleMappings = async (actor: FastifyRequest["permission"]) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
// TODO: will need to change if we add support for ldap, oidc, etc.
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Scim);
|
||||
|
||||
const mappings = await externalGroupOrgRoleMappingDAL.find({
|
||||
orgId: actor.orgId
|
||||
});
|
||||
|
||||
return mappings;
|
||||
};
|
||||
|
||||
const updateExternalGroupOrgRoleMappings = async (
|
||||
dto: TSyncExternalGroupOrgMembershipRoleMappingsDTO,
|
||||
actor: FastifyRequest["permission"]
|
||||
) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
actor.authMethod,
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
// TODO: will need to change if we add support for ldap, oidc, etc.
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||
|
||||
const mappings = await constructGroupOrgMembershipRoleMappings({
|
||||
mappingsDTO: dto.mappings,
|
||||
orgRoleDAL,
|
||||
licenseService,
|
||||
orgId: actor.orgId
|
||||
});
|
||||
|
||||
const data = await externalGroupOrgRoleMappingDAL.updateExternalGroupOrgRoleMappingForOrg(actor.orgId, mappings);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return {
|
||||
updateExternalGroupOrgRoleMappings,
|
||||
listExternalGroupOrgRoleMappings
|
||||
};
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export type TExternalGroupOrgMembershipRoleMappingDTO = {
|
||||
groupName: string;
|
||||
roleSlug: string;
|
||||
};
|
||||
|
||||
export type TSyncExternalGroupOrgMembershipRoleMappingsDTO = {
|
||||
mappings: TExternalGroupOrgMembershipRoleMappingDTO[];
|
||||
};
|
||||
@@ -5,6 +5,9 @@ import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||
|
||||
const RESERVED_ORG_ROLE_SLUGS = Object.values(OrgMembershipRole).filter((role) => role !== "custom");
|
||||
|
||||
export const isCustomOrgRole = (membershipSlug: string) =>
|
||||
!RESERVED_ORG_ROLE_SLUGS.includes(membershipSlug as OrgMembershipRole);
|
||||
|
||||
// this is only for updating an org
|
||||
export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
|
||||
membershipRoleSlug,
|
||||
@@ -17,9 +20,7 @@ export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
|
||||
orgRoleDAL: TOrgRoleDALFactory;
|
||||
plan: TFeatureSet;
|
||||
}) => {
|
||||
const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(membershipRoleSlug as OrgMembershipRole);
|
||||
|
||||
if (isCustomRole) {
|
||||
if (isCustomOrgRole(membershipRoleSlug)) {
|
||||
if (!plan?.rbac)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
@@ -41,9 +42,7 @@ export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
|
||||
export const getDefaultOrgMembershipRole = async (
|
||||
defaultOrgMembershipRole: string // can either be ID or reserved slug
|
||||
) => {
|
||||
const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(defaultOrgMembershipRole as OrgMembershipRole);
|
||||
|
||||
if (isCustomRole)
|
||||
if (isCustomOrgRole(defaultOrgMembershipRole))
|
||||
return {
|
||||
roleId: defaultOrgMembershipRole,
|
||||
role: OrgMembershipRole.Custom
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TExternalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
|
||||
import { ActorAuthMethod } from "../auth/auth-type";
|
||||
@@ -20,11 +21,17 @@ type TOrgRoleServiceFactoryDep = {
|
||||
orgRoleDAL: TOrgRoleDALFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
orgDAL: TOrgDALFactory;
|
||||
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
|
||||
};
|
||||
|
||||
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
|
||||
|
||||
export const orgRoleServiceFactory = ({ orgRoleDAL, orgDAL, permissionService }: TOrgRoleServiceFactoryDep) => {
|
||||
export const orgRoleServiceFactory = ({
|
||||
orgRoleDAL,
|
||||
orgDAL,
|
||||
permissionService,
|
||||
externalGroupOrgRoleMappingDAL
|
||||
}: TOrgRoleServiceFactoryDep) => {
|
||||
const createRole = async (
|
||||
userId: string,
|
||||
orgId: string,
|
||||
@@ -144,6 +151,17 @@ export const orgRoleServiceFactory = ({ orgRoleDAL, orgDAL, permissionService }:
|
||||
message: "Cannot delete default org membership role. Please re-assign and try again."
|
||||
});
|
||||
|
||||
const externalGroupMapping = await externalGroupOrgRoleMappingDAL.findOne({
|
||||
orgId,
|
||||
roleId
|
||||
});
|
||||
|
||||
if (externalGroupMapping)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Cannot delete role assigned to external group organization role mapping. Please re-assign external mapping and try again."
|
||||
});
|
||||
|
||||
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
|
||||
if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user