mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 07:26:45 +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 { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
import { TCmekServiceFactory } from "@app/services/cmek/cmek-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 { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
@@ -185,6 +186,7 @@ declare module "fastify" {
|
|||||||
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
workflowIntegration: TWorkflowIntegrationServiceFactory;
|
||||||
cmek: TCmekServiceFactory;
|
cmek: TCmekServiceFactory;
|
||||||
migration: TExternalMigrationServiceFactory;
|
migration: TExternalMigrationServiceFactory;
|
||||||
|
externalGroupOrgRoleMapping: TExternalGroupOrgRoleMappingServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
|||||||
Vendored
+10
@@ -336,6 +336,11 @@ import {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
TWorkflowIntegrationsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
TExternalGroupOrgRoleMappings,
|
||||||
|
TExternalGroupOrgRoleMappingsInsert,
|
||||||
|
TExternalGroupOrgRoleMappingsUpdate
|
||||||
|
} from "@app/db/schemas/external-group-org-role-mappings";
|
||||||
import {
|
import {
|
||||||
TSecretV2TagJunction,
|
TSecretV2TagJunction,
|
||||||
TSecretV2TagJunctionInsert,
|
TSecretV2TagJunctionInsert,
|
||||||
@@ -808,5 +813,10 @@ declare module "knex/types/tables" {
|
|||||||
TWorkflowIntegrationsInsert,
|
TWorkflowIntegrationsInsert,
|
||||||
TWorkflowIntegrationsUpdate
|
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",
|
Groups = "groups",
|
||||||
GroupProjectMembership = "group_project_memberships",
|
GroupProjectMembership = "group_project_memberships",
|
||||||
GroupProjectMembershipRole = "group_project_membership_roles",
|
GroupProjectMembershipRole = "group_project_membership_roles",
|
||||||
|
ExternalGroupOrgRoleMapping = "external_group_org_role_mappings",
|
||||||
UserGroupMembership = "user_group_membership",
|
UserGroupMembership = "user_group_membership",
|
||||||
UserAliases = "user_aliases",
|
UserAliases = "user_aliases",
|
||||||
UserEncryptionKey = "user_encryption_keys",
|
UserEncryptionKey = "user_encryption_keys",
|
||||||
|
|||||||
@@ -190,7 +190,9 @@ export enum EventType {
|
|||||||
DELETE_CMEK = "delete-cmek",
|
DELETE_CMEK = "delete-cmek",
|
||||||
GET_CMEKS = "get-cmeks",
|
GET_CMEKS = "get-cmeks",
|
||||||
CMEK_ENCRYPT = "cmek-encrypt",
|
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 {
|
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 =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -1750,4 +1764,6 @@ export type Event =
|
|||||||
| DeleteCmekEvent
|
| DeleteCmekEvent
|
||||||
| GetCmeksEvent
|
| GetCmeksEvent
|
||||||
| CmekEncryptEvent
|
| CmekEncryptEvent
|
||||||
| CmekDecryptEvent;
|
| CmekDecryptEvent
|
||||||
|
| GetExternalGroupOrgRoleMappingsEvent
|
||||||
|
| UpdateExternalGroupOrgRoleMappingsEvent;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { scimPatch } from "scim-patch";
|
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 { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
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 { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
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 { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
|
import { deleteOrgMembershipFn } from "@app/services/org/org-fns";
|
||||||
@@ -71,7 +72,10 @@ type TScimServiceFactoryDep = {
|
|||||||
| "transaction"
|
| "transaction"
|
||||||
| "updateMembershipById"
|
| "updateMembershipById"
|
||||||
>;
|
>;
|
||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
|
orgMembershipDAL: Pick<
|
||||||
|
TOrgMembershipDALFactory,
|
||||||
|
"find" | "findOne" | "create" | "updateById" | "findById" | "update"
|
||||||
|
>;
|
||||||
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
|
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
||||||
groupDAL: Pick<
|
groupDAL: Pick<
|
||||||
@@ -102,6 +106,7 @@ type TScimServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
|
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
|
||||||
@@ -122,7 +127,8 @@ export const scimServiceFactory = ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectUserAdditionalPrivilegeDAL,
|
projectUserAdditionalPrivilegeDAL,
|
||||||
smtpService
|
smtpService,
|
||||||
|
externalGroupOrgRoleMappingDAL
|
||||||
}: TScimServiceFactoryDep) => {
|
}: TScimServiceFactoryDep) => {
|
||||||
const createScimToken = async ({
|
const createScimToken = async ({
|
||||||
actor,
|
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 createScimGroup = async ({ displayName, orgId, members }: TCreateScimGroupDTO) => {
|
||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
if (!plan.groups)
|
if (!plan.groups)
|
||||||
@@ -745,6 +788,8 @@ export const scimServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await $syncNewMembersRoles(group, members);
|
||||||
|
|
||||||
return { group, newMembers };
|
return { group, newMembers };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,22 +865,41 @@ export const scimServiceFactory = ({
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
{ displayName, members = [] }: { displayName: string; members: { value: string }[] }
|
{ displayName, members = [] }: { displayName: string; members: { value: string }[] }
|
||||||
) => {
|
) => {
|
||||||
const updatedGroup = await groupDAL.transaction(async (tx) => {
|
let group = await groupDAL.findOne({
|
||||||
const [group] = await groupDAL.update(
|
id: groupId,
|
||||||
{
|
orgId
|
||||||
id: groupId,
|
});
|
||||||
orgId
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: displayName
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
throw new ScimRequestError({
|
throw new ScimRequestError({
|
||||||
detail: "Group Not Found",
|
detail: "Group Not Found",
|
||||||
status: 404
|
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
|
const orgMemberships = members.length
|
||||||
@@ -892,6 +956,8 @@ export const scimServiceFactory = ({
|
|||||||
return group;
|
return group;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await $syncNewMembersRoles(group, members);
|
||||||
|
|
||||||
return updatedGroup;
|
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 { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
import { cmekServiceFactory } from "@app/services/cmek/cmek-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 { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
|
||||||
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
@@ -336,6 +338,8 @@ export const registerRoutes = async (
|
|||||||
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
const projectSlackConfigDAL = projectSlackConfigDALFactory(db);
|
||||||
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
|
const workflowIntegrationDAL = workflowIntegrationDALFactory(db);
|
||||||
|
|
||||||
|
const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@@ -442,7 +446,8 @@ export const registerRoutes = async (
|
|||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
smtpService
|
smtpService,
|
||||||
|
externalGroupOrgRoleMappingDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const ldapService = ldapConfigServiceFactory({
|
const ldapService = ldapConfigServiceFactory({
|
||||||
@@ -537,7 +542,12 @@ export const registerRoutes = async (
|
|||||||
orgService,
|
orgService,
|
||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL, orgDAL });
|
const orgRoleService = orgRoleServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
orgRoleDAL,
|
||||||
|
orgDAL,
|
||||||
|
externalGroupOrgRoleMappingDAL
|
||||||
|
});
|
||||||
const superAdminService = superAdminServiceFactory({
|
const superAdminService = superAdminServiceFactory({
|
||||||
userDAL,
|
userDAL,
|
||||||
authService: loginService,
|
authService: loginService,
|
||||||
@@ -1231,6 +1241,13 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
licenseService,
|
||||||
|
orgRoleDAL,
|
||||||
|
externalGroupOrgRoleMappingDAL
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
//
|
//
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@@ -1316,7 +1333,8 @@ export const registerRoutes = async (
|
|||||||
orgAdmin: orgAdminService,
|
orgAdmin: orgAdminService,
|
||||||
slack: slackService,
|
slack: slackService,
|
||||||
workflowIntegration: workflowIntegrationService,
|
workflowIntegration: workflowIntegrationService,
|
||||||
migration: migrationService
|
migration: migrationService,
|
||||||
|
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
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 { registerCaRouter } from "./certificate-authority-router";
|
||||||
import { registerCertRouter } from "./certificate-router";
|
import { registerCertRouter } from "./certificate-router";
|
||||||
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
import { registerCertificateTemplateRouter } from "./certificate-template-router";
|
||||||
|
import { registerExternalGroupOrgRoleMappingRouter } from "./external-group-org-role-mapping-router";
|
||||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||||
import { registerIdentityAzureAuthRouter } from "./identity-azure-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(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
||||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||||
await server.register(registerCmekRouter, { prefix: "/kms" });
|
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");
|
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
|
// this is only for updating an org
|
||||||
export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
|
export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
|
||||||
membershipRoleSlug,
|
membershipRoleSlug,
|
||||||
@@ -17,9 +20,7 @@ export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
|
|||||||
orgRoleDAL: TOrgRoleDALFactory;
|
orgRoleDAL: TOrgRoleDALFactory;
|
||||||
plan: TFeatureSet;
|
plan: TFeatureSet;
|
||||||
}) => {
|
}) => {
|
||||||
const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(membershipRoleSlug as OrgMembershipRole);
|
if (isCustomOrgRole(membershipRoleSlug)) {
|
||||||
|
|
||||||
if (isCustomRole) {
|
|
||||||
if (!plan?.rbac)
|
if (!plan?.rbac)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message:
|
message:
|
||||||
@@ -41,9 +42,7 @@ export const getDefaultOrgMembershipRoleForUpdateOrg = async ({
|
|||||||
export const getDefaultOrgMembershipRole = async (
|
export const getDefaultOrgMembershipRole = async (
|
||||||
defaultOrgMembershipRole: string // can either be ID or reserved slug
|
defaultOrgMembershipRole: string // can either be ID or reserved slug
|
||||||
) => {
|
) => {
|
||||||
const isCustomRole = !RESERVED_ORG_ROLE_SLUGS.includes(defaultOrgMembershipRole as OrgMembershipRole);
|
if (isCustomOrgRole(defaultOrgMembershipRole))
|
||||||
|
|
||||||
if (isCustomRole)
|
|
||||||
return {
|
return {
|
||||||
roleId: defaultOrgMembershipRole,
|
roleId: defaultOrgMembershipRole,
|
||||||
role: OrgMembershipRole.Custom
|
role: OrgMembershipRole.Custom
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@app/ee/services/permission/org-permission";
|
} from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
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 { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
|
||||||
import { ActorAuthMethod } from "../auth/auth-type";
|
import { ActorAuthMethod } from "../auth/auth-type";
|
||||||
@@ -20,11 +21,17 @@ type TOrgRoleServiceFactoryDep = {
|
|||||||
orgRoleDAL: TOrgRoleDALFactory;
|
orgRoleDAL: TOrgRoleDALFactory;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
|
externalGroupOrgRoleMappingDAL: TExternalGroupOrgRoleMappingDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
|
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
|
||||||
|
|
||||||
export const orgRoleServiceFactory = ({ orgRoleDAL, orgDAL, permissionService }: TOrgRoleServiceFactoryDep) => {
|
export const orgRoleServiceFactory = ({
|
||||||
|
orgRoleDAL,
|
||||||
|
orgDAL,
|
||||||
|
permissionService,
|
||||||
|
externalGroupOrgRoleMappingDAL
|
||||||
|
}: TOrgRoleServiceFactoryDep) => {
|
||||||
const createRole = async (
|
const createRole = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
orgId: 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."
|
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 });
|
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
|
||||||
if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" });
|
if (!deletedRole) throw new NotFoundError({ message: "Organization role not found", name: "Update role" });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
enum OrgMembershipRole {
|
||||||
|
Admin = "admin",
|
||||||
|
Member = "member",
|
||||||
|
NoAccess = "no-access"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isCustomOrgRole = (slug: string) =>
|
||||||
|
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./mutations";
|
||||||
|
export * from "./queries";
|
||||||
|
export * from "./types";
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { externalGroupOrgRoleMappingKeys } from "@app/hooks/api/externalGroupOrgRoleMappings/queries";
|
||||||
|
import { TSyncExternalGroupOrgRoleMappingsDTO } from "@app/hooks/api/externalGroupOrgRoleMappings/types";
|
||||||
|
|
||||||
|
export const useUpdateExternalGroupOrgRoleMappings = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload: TSyncExternalGroupOrgRoleMappingsDTO) => {
|
||||||
|
const { data } = await apiRequest.post("/api/v1/external-group-mappings", payload);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(externalGroupOrgRoleMappingKeys.list());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { TExternalGroupOrgRoleMappingList } from "@app/hooks/api/externalGroupOrgRoleMappings/types";
|
||||||
|
|
||||||
|
export const externalGroupOrgRoleMappingKeys = {
|
||||||
|
all: ["external-group-org-role-mapping"] as const,
|
||||||
|
list: () => [...externalGroupOrgRoleMappingKeys.all, "list"] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetExternalGroupOrgRoleMappings = (
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TExternalGroupOrgRoleMappingList,
|
||||||
|
unknown,
|
||||||
|
TExternalGroupOrgRoleMappingList,
|
||||||
|
ReturnType<typeof externalGroupOrgRoleMappingKeys.list>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: externalGroupOrgRoleMappingKeys.list(),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TExternalGroupOrgRoleMappingList>(
|
||||||
|
"/api/v1/external-group-mappings"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export type TSyncExternalGroupOrgRoleMappingsDTO = {
|
||||||
|
mappings: {
|
||||||
|
groupName: string;
|
||||||
|
roleSlug: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TExternalGroupOrgRoleMapping = {
|
||||||
|
id: string;
|
||||||
|
groupName: string;
|
||||||
|
role: string;
|
||||||
|
roleId: string;
|
||||||
|
orgId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TExternalGroupOrgRoleMappingList = TExternalGroupOrgRoleMapping[];
|
||||||
+1
-1
@@ -25,6 +25,7 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
|
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||||
import {
|
import {
|
||||||
useAddUsersToOrg,
|
useAddUsersToOrg,
|
||||||
useFetchServerStatus,
|
useFetchServerStatus,
|
||||||
@@ -34,7 +35,6 @@ import {
|
|||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
import { isCustomOrgRole } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable";
|
|
||||||
|
|
||||||
import { OrgInviteLink } from "./OrgInviteLink";
|
import { OrgInviteLink } from "./OrgInviteLink";
|
||||||
|
|
||||||
|
|||||||
@@ -30,20 +30,12 @@ import {
|
|||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription
|
useSubscription
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
|
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
import { useDeleteOrgRole, useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
||||||
import { TOrgRole } from "@app/hooks/api/roles/types";
|
import { TOrgRole } from "@app/hooks/api/roles/types";
|
||||||
import { RoleModal } from "@app/views/Org/RolePage/components";
|
import { RoleModal } from "@app/views/Org/RolePage/components";
|
||||||
|
|
||||||
enum OrgMembershipRole {
|
|
||||||
Admin = "admin",
|
|
||||||
Member = "member",
|
|
||||||
NoAccess = "no-access"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isCustomOrgRole = (slug: string) =>
|
|
||||||
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
|
|
||||||
|
|
||||||
export const OrgRoleTable = () => {
|
export const OrgRoleTable = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
|
|||||||
+213
@@ -0,0 +1,213 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Spinner
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useOrgPermission
|
||||||
|
} from "@app/context";
|
||||||
|
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||||
|
import { useGetOrgRoles } from "@app/hooks/api";
|
||||||
|
import {
|
||||||
|
useGetExternalGroupOrgRoleMappings,
|
||||||
|
useUpdateExternalGroupOrgRoleMappings
|
||||||
|
} from "@app/hooks/api/externalGroupOrgRoleMappings";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
mappings: z
|
||||||
|
.object({
|
||||||
|
groupName: z.string().trim().min(1, { message: "Group name is required" }),
|
||||||
|
roleSlug: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const ExternalGroupOrgRoleMappings = () => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { data: roles, isLoading: isRolesLoading } = useGetOrgRoles(currentOrg?.id!);
|
||||||
|
const { data: mappings } = useGetExternalGroupOrgRoleMappings();
|
||||||
|
const updateMappings = useUpdateExternalGroupOrgRoleMappings();
|
||||||
|
const { permission } = useOrgPermission();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isDirty },
|
||||||
|
handleSubmit,
|
||||||
|
reset
|
||||||
|
} = useForm<TForm>({
|
||||||
|
defaultValues: { mappings: [] },
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mappings || !roles) return;
|
||||||
|
|
||||||
|
reset({
|
||||||
|
mappings: mappings.map((mapping) => ({
|
||||||
|
groupName: mapping.groupName,
|
||||||
|
roleSlug:
|
||||||
|
mapping.role === "custom"
|
||||||
|
? roles.find((role) => mapping.roleId === role.id)!.slug
|
||||||
|
: mapping.role
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}, [mappings, roles]);
|
||||||
|
|
||||||
|
const mappingField = useFieldArray({ control, name: "mappings" });
|
||||||
|
|
||||||
|
const handleUpdateMappings = async (form: TForm) => {
|
||||||
|
try {
|
||||||
|
await updateMappings.mutateAsync(form);
|
||||||
|
createNotification({
|
||||||
|
text: "Group organization role mappings updated.",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update group organization role mappings.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableScimEdit = permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-4">
|
||||||
|
<h2 className="text-md text-mineshaft-100">SCIM Group to Organization Role Mappings</h2>
|
||||||
|
<p className="text-sm text-mineshaft-300">
|
||||||
|
Assign newly provisioned users a default organization role based on their SCIM group.
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateMappings)} className="pt-4">
|
||||||
|
{isRolesLoading || isRolesLoading ? (
|
||||||
|
<Spinner className="self-center" size="sm" />
|
||||||
|
) : (
|
||||||
|
<div className="mb-2 flex flex-col space-y-2">
|
||||||
|
{mappingField.fields.map(({ id: scopeFieldId }, i) => (
|
||||||
|
<div key={scopeFieldId} className="flex items-end space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
{i === 0 && (
|
||||||
|
<FormLabel
|
||||||
|
label="SCIM Group Name"
|
||||||
|
className="text-xs text-mineshaft-400"
|
||||||
|
tooltipClassName="max-w-md whitespace-pre-line"
|
||||||
|
tooltipText="The name associated with this group in your SCIM provider"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`mappings.${i}.groupName`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
isDisabled={disableScimEdit}
|
||||||
|
{...field}
|
||||||
|
placeholder="SCIM group identifier..."
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
{i === 0 && (
|
||||||
|
<span className="text-xs text-mineshaft-400">
|
||||||
|
Role to Assign Users in this Group
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`mappings.${i}.roleSlug`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={disableScimEdit}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{roles?.map((role) => (
|
||||||
|
<SelectItem value={role.slug} key={`role-${role.id}`}>
|
||||||
|
{role.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete key"
|
||||||
|
className="bottom-0.5 h-9"
|
||||||
|
variant="outline_bg"
|
||||||
|
isDisabled={disableScimEdit}
|
||||||
|
onClick={() => {
|
||||||
|
mappingField.remove(i);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
isDisabled={disableScimEdit}
|
||||||
|
onClick={() =>
|
||||||
|
mappingField.append({
|
||||||
|
groupName: "",
|
||||||
|
roleSlug: isCustomOrgRole(currentOrg!.defaultMembershipRole)
|
||||||
|
? roles?.find((role) => currentOrg?.defaultMembershipRole === role.id)?.slug!
|
||||||
|
: currentOrg!.defaultMembershipRole
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Mapping
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isDirty && (
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Scim}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button isDisabled={!isAllowed} colorSchema="secondary" type="submit">
|
||||||
|
Update Mappings
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useUpdateOrg } from "@app/hooks/api";
|
import { useUpdateOrg } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
import { ExternalGroupOrgRoleMappings } from "@app/views/Settings/OrgSettingsPage/components/OrgAuthTab/ExternalGroupOrgRoleMappings";
|
||||||
|
|
||||||
import { ScimTokenModal } from "./ScimTokenModal";
|
import { ScimTokenModal } from "./ScimTokenModal";
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ export const OrgScimSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-mineshaft-300">Manage SCIM configuration</p>
|
<p className="text-sm text-mineshaft-300">Manage SCIM configuration</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ExternalGroupOrgRoleMappings />
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<h2 className="text-md text-mineshaft-100">Enable SCIM</h2>
|
<h2 className="text-md text-mineshaft-100">Enable SCIM</h2>
|
||||||
|
|||||||
+1
-1
@@ -12,8 +12,8 @@ import {
|
|||||||
useOrganization,
|
useOrganization,
|
||||||
useOrgPermission
|
useOrgPermission
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
|
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||||
import { useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
import { useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
||||||
import { isCustomOrgRole } from "@app/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleTable";
|
|
||||||
|
|
||||||
const formSchema = yup.object({
|
const formSchema = yup.object({
|
||||||
name: yup
|
name: yup
|
||||||
|
|||||||
Reference in New Issue
Block a user