From 961c6391a827ff38dee025e5e40c1f3368bbd267 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 23 Apr 2024 13:58:23 -0700 Subject: [PATCH] Complete LDAP group mapping data structure + frontend/backend --- backend/src/@types/knex.d.ts | 4 + .../20240423023203_ldap-config-groups.ts | 16 ++ backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/ldap-group-maps.ts | 19 ++ backend/src/db/schemas/models.ts | 1 + backend/src/ee/routes/v1/ldap-router.ts | 118 ++++++++- .../ldap-config/ldap-config-service.ts | 121 ++++++++- .../services/ldap-config/ldap-config-types.ts | 19 ++ .../ldap-config/ldap-group-map-dal.ts | 11 + backend/src/server/routes/index.ts | 4 + frontend/src/hooks/api/ldapConfig/index.tsx | 8 +- .../src/hooks/api/ldapConfig/mutations.tsx | 45 ++++ frontend/src/hooks/api/ldapConfig/queries.tsx | 21 +- frontend/src/hooks/api/ldapConfig/types.ts | 6 + .../OrgAuthTab/LDAPGroupMapModal.tsx | 238 ++++++++++++++++++ .../components/OrgAuthTab/OrgLDAPSection.tsx | 37 +++ 16 files changed, 661 insertions(+), 8 deletions(-) create mode 100644 backend/src/db/schemas/ldap-group-maps.ts create mode 100644 backend/src/ee/services/ldap-config/ldap-group-map-dal.ts create mode 100644 frontend/src/hooks/api/ldapConfig/types.ts create mode 100644 frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPGroupMapModal.tsx diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 2c8b8be5a..8845c1d01 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -74,6 +74,9 @@ import { TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate, + TLdapGroupMaps, + TLdapGroupMapsInsert, + TLdapGroupMapsUpdate, TOrganizations, TOrganizationsInsert, TOrganizationsUpdate, @@ -398,6 +401,7 @@ declare module "knex/types/tables" { >; [TableName.SamlConfig]: Knex.CompositeTableType; [TableName.LdapConfig]: Knex.CompositeTableType; + [TableName.LdapGroupMap]: Knex.CompositeTableType; [TableName.OrgBot]: Knex.CompositeTableType; [TableName.AuditLog]: Knex.CompositeTableType; [TableName.GitAppInstallSession]: Knex.CompositeTableType< diff --git a/backend/src/db/migrations/20240423023203_ldap-config-groups.ts b/backend/src/db/migrations/20240423023203_ldap-config-groups.ts index 732736930..43d1a389b 100644 --- a/backend/src/db/migrations/20240423023203_ldap-config-groups.ts +++ b/backend/src/db/migrations/20240423023203_ldap-config-groups.ts @@ -1,8 +1,22 @@ import { Knex } from "knex"; import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.LdapGroupMap))) { + await knex.schema.createTable(TableName.LdapGroupMap, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("ldapConfigId").notNullable(); + t.foreign("ldapConfigId").references("id").inTable(TableName.LdapConfig).onDelete("CASCADE"); + t.string("ldapGroupCN").notNullable(); + t.string("groupSlug").notNullable(); + t.unique(["ldapGroupCN", "groupSlug", "ldapConfigId"]); + }); + } + + await createOnUpdateTrigger(knex, TableName.LdapGroupMap); + await knex.schema.alterTable(TableName.LdapConfig, (t) => { t.string("groupSearchBase").notNullable().defaultTo(""); t.string("groupSearchFilter").notNullable().defaultTo(""); @@ -10,6 +24,8 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.LdapGroupMap); + await dropOnUpdateTrigger(knex, TableName.LdapGroupMap); await knex.schema.alterTable(TableName.LdapConfig, (t) => { t.dropColumn("groupSearchBase"); t.dropColumn("groupSearchFilter"); diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index b9dab06ba..30d6208b8 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -22,6 +22,7 @@ export * from "./incident-contacts"; export * from "./integration-auths"; export * from "./integrations"; export * from "./ldap-configs"; +export * from "./ldap-group-maps"; export * from "./models"; export * from "./org-bots"; export * from "./org-memberships"; diff --git a/backend/src/db/schemas/ldap-group-maps.ts b/backend/src/db/schemas/ldap-group-maps.ts new file mode 100644 index 000000000..3cb744238 --- /dev/null +++ b/backend/src/db/schemas/ldap-group-maps.ts @@ -0,0 +1,19 @@ +// 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 LdapGroupMapsSchema = z.object({ + id: z.string().uuid(), + ldapConfigId: z.string().uuid(), + ldapGroupCN: z.string(), + groupSlug: z.string() +}); + +export type TLdapGroupMaps = z.infer; +export type TLdapGroupMapsInsert = Omit, TImmutableDBKeys>; +export type TLdapGroupMapsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index d5cf1b886..ea70dccdb 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -60,6 +60,7 @@ export enum TableName { SecretRotationOutput = "secret_rotation_outputs", SamlConfig = "saml_configs", LdapConfig = "ldap_configs", + LdapGroupMap = "ldap_group_maps", AuditLog = "audit_logs", GitAppInstallSession = "git_app_install_sessions", GitAppOrg = "git_app_org", diff --git a/backend/src/ee/routes/v1/ldap-router.ts b/backend/src/ee/routes/v1/ldap-router.ts index 0c531146f..dd0bde278 100644 --- a/backend/src/ee/routes/v1/ldap-router.ts +++ b/backend/src/ee/routes/v1/ldap-router.ts @@ -15,7 +15,7 @@ import ldapjs from "ldapjs"; import LdapStrategy from "passport-ldapauth"; import { z } from "zod"; -import { LdapConfigsSchema } from "@app/db/schemas"; +import { LdapConfigsSchema, LdapGroupMapsSchema } from "@app/db/schemas"; import { searchGroups } from "@app/ee/services/ldap-config/ldap-fns"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; @@ -67,6 +67,28 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { try { const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as LDAPConfig; + if (!ldapConfig.groupSearchFilter || !ldapConfig.groupSearchBase) { + // If group search values are not provided, proceed directly to LDAP login + return await server.services.ldap + .ldapLogin({ + externalId: user.uidNumber, + username: user.uid, + firstName: user.givenName, + lastName: user.sn, + emails: user.mail ? [user.mail] : [], + relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState, + orgId: (req as unknown as FastifyRequest).ldapConfig.organization + }) + .then(({ isUserCompleted, providerAuthToken }) => { + cb(null, { isUserCompleted, providerAuthToken }); + }) + .catch((err) => { + logger.error(err); + cb(err, false); + }); + } + + // query for groups const ldapClient = ldapjs.createClient({ url: ldapConfig.url, bindDN: ldapConfig.bindDN, @@ -85,7 +107,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { const searchFilter = groupFilter.replace("{{.Username}}", user.uid).replace("{{.UserDN}}", user.dn); searchGroups(ldapClient, searchFilter, ldapConfig.groupSearchBase) - .then(() => { + .then((groups) => { // groups here ldapClient.unbind(); return server.services.ldap.ldapLogin({ @@ -94,6 +116,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { firstName: user.givenName, lastName: user.sn, emails: user.mail ? [user.mail] : [], + groups, relayState: ((req as unknown as FastifyRequest).body as { RelayState?: string }).RelayState, orgId: (req as unknown as FastifyRequest).ldapConfig.organization }); @@ -256,4 +279,95 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => { return ldap; } }); + + server.route({ + method: "GET", + url: "/config/:configId/group-maps", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + configId: z.string().trim() + }), + response: { + 200: z.array(LdapGroupMapsSchema) + } + }, + handler: async (req) => { + const ldapGroupMaps = await server.services.ldap.getLdapGroupMaps({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ldapConfigId: req.params.configId + }); + return ldapGroupMaps; + } + }); + + server.route({ + method: "POST", + url: "/config/:configId/group-maps", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + configId: z.string().trim() + }), + body: z.object({ + ldapGroupCN: z.string().trim(), + groupSlug: z.string().trim() + }), + response: { + 200: LdapGroupMapsSchema + } + }, + handler: async (req) => { + const ldapGroupMap = await server.services.ldap.createLdapGroupMap({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ldapConfigId: req.params.configId, + ...req.body + }); + return ldapGroupMap; + } + }); + + server.route({ + method: "DELETE", + url: "/config/:configId/group-maps/:groupMapId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + configId: z.string().trim(), + groupMapId: z.string().trim() + }), + response: { + 200: LdapGroupMapsSchema + } + }, + handler: async (req) => { + const ldapGroupMap = await server.services.ldap.deleteLdapGroupMap({ + actor: req.permission.type, + actorId: req.permission.id, + orgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ldapConfigId: req.params.configId, + ldapGroupMapId: req.params.groupMapId + }); + return ldapGroupMap; + } + }); }; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 53631d985..f4f66830e 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import jwt from "jsonwebtoken"; import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TLdapConfigsUpdate } from "@app/db/schemas"; +import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric, @@ -23,15 +24,26 @@ import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { TLdapConfigDALFactory } from "./ldap-config-dal"; -import { TCreateLdapCfgDTO, TGetLdapCfgDTO, TLdapLoginDTO, TUpdateLdapCfgDTO } from "./ldap-config-types"; +import { + TCreateLdapCfgDTO, + TCreateLdapGroupMapDTO, + TDeleteLdapGroupMapDTO, + TGetLdapCfgDTO, + TGetLdapGroupMapsDTO, + TLdapLoginDTO, + TUpdateLdapCfgDTO +} from "./ldap-config-types"; +import { TLdapGroupMapDALFactory } from "./ldap-group-map-dal"; type TLdapConfigServiceFactoryDep = { - ldapConfigDAL: TLdapConfigDALFactory; + ldapConfigDAL: Pick; + ldapGroupMapDAL: Pick; orgDAL: Pick< TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById" >; orgBotDAL: Pick; + groupDAL: TGroupDALFactory; // TODO: Pick userDAL: Pick; userAliasDAL: Pick; permissionService: Pick; @@ -42,6 +54,7 @@ export type TLdapConfigServiceFactory = ReturnType group.cn) + // } + // }); + + // console.log("found matching groups"); + // } + const isUserCompleted = Boolean(user.isAccepted); const providerAuthToken = jwt.sign( @@ -434,6 +465,87 @@ export const ldapConfigServiceFactory = ({ return { isUserCompleted, providerAuthToken }; }; + const getLdapGroupMaps = async ({ + ldapConfigId, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }: TGetLdapGroupMapsDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); + + const ldapConfig = await ldapConfigDAL.findOne({ + id: ldapConfigId, + orgId + }); + + if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" }); + + const groupMaps = await ldapGroupMapDAL.find({ + ldapConfigId + }); + + return groupMaps; + }; + + const createLdapGroupMap = async ({ + ldapConfigId, + ldapGroupCN, + groupSlug, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }: TCreateLdapGroupMapDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); + + const ldapConfig = await ldapConfigDAL.findOne({ + id: ldapConfigId, + orgId + }); + + if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" }); + + const groupMap = await ldapGroupMapDAL.create({ + ldapConfigId, + ldapGroupCN, + groupSlug + }); + + return groupMap; + }; + + const deleteLdapGroupMap = async ({ + ldapConfigId, + ldapGroupMapId, + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + }: TDeleteLdapGroupMapDTO) => { + const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap); + + const ldapConfig = await ldapConfigDAL.findOne({ + id: ldapConfigId, + orgId + }); + + if (!ldapConfig) throw new BadRequestError({ message: "Failed to find organization LDAP data" }); + + const [deletedGroupMap] = await ldapGroupMapDAL.delete({ + ldapConfigId: ldapConfig.id, + id: ldapGroupMapId + }); + + return deletedGroupMap; + }; + return { createLdapCfg, updateLdapCfg, @@ -441,6 +553,9 @@ export const ldapConfigServiceFactory = ({ getLdapCfg, // getLdapPassportOpts, ldapLogin, - bootLdap + bootLdap, + getLdapGroupMaps, + createLdapGroupMap, + deleteLdapGroupMap }; }; diff --git a/backend/src/ee/services/ldap-config/ldap-config-types.ts b/backend/src/ee/services/ldap-config/ldap-config-types.ts index 7cadb489c..345e6fba1 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-types.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-types.ts @@ -37,5 +37,24 @@ export type TLdapLoginDTO = { lastName: string; emails: string[]; orgId: string; + groups?: { + dn: string; + cn: string; + }[]; relayState?: string; }; + +export type TGetLdapGroupMapsDTO = { + ldapConfigId: string; +} & TOrgPermission; + +export type TCreateLdapGroupMapDTO = { + ldapConfigId: string; + ldapGroupCN: string; + groupSlug: string; +} & TOrgPermission; + +export type TDeleteLdapGroupMapDTO = { + ldapConfigId: string; + ldapGroupMapId: string; +} & TOrgPermission; diff --git a/backend/src/ee/services/ldap-config/ldap-group-map-dal.ts b/backend/src/ee/services/ldap-config/ldap-group-map-dal.ts new file mode 100644 index 000000000..1ecb5c9ba --- /dev/null +++ b/backend/src/ee/services/ldap-config/ldap-group-map-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TLdapGroupMapDALFactory = ReturnType; + +export const ldapGroupMapDALFactory = (db: TDbClient) => { + const ldapGroupMapOrm = ormify(db, TableName.LdapGroupMap); + + return { ...ldapGroupMapOrm }; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 5ed2589f6..d53830ae2 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -18,6 +18,7 @@ import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/i import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; +import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal"; import { licenseDALFactory } from "@app/ee/services/license/license-dal"; import { licenseServiceFactory } from "@app/ee/services/license/license-service"; import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; @@ -200,6 +201,7 @@ export const registerRoutes = async ( const samlConfigDAL = samlConfigDALFactory(db); const scimDAL = scimDALFactory(db); const ldapConfigDAL = ldapConfigDALFactory(db); + const ldapGroupMapDAL = ldapGroupMapDALFactory(db); const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); @@ -300,8 +302,10 @@ export const registerRoutes = async ( const ldapService = ldapConfigServiceFactory({ ldapConfigDAL, + ldapGroupMapDAL, orgDAL, orgBotDAL, + groupDAL, userDAL, userAliasDAL, permissionService, diff --git a/frontend/src/hooks/api/ldapConfig/index.tsx b/frontend/src/hooks/api/ldapConfig/index.tsx index 9d8118919..26af75089 100644 --- a/frontend/src/hooks/api/ldapConfig/index.tsx +++ b/frontend/src/hooks/api/ldapConfig/index.tsx @@ -1,2 +1,6 @@ -export { useCreateLDAPConfig, useUpdateLDAPConfig } from "./mutations"; -export { useGetLDAPConfig } from "./queries"; +export { + useCreateLDAPConfig, + useCreateLDAPGroupMapping, + useDeleteLDAPGroupMapping, + useUpdateLDAPConfig} from "./mutations"; +export { useGetLDAPConfig, useGetLDAPGroupMaps } from "./queries"; diff --git a/frontend/src/hooks/api/ldapConfig/mutations.tsx b/frontend/src/hooks/api/ldapConfig/mutations.tsx index be24f2e84..50651b367 100644 --- a/frontend/src/hooks/api/ldapConfig/mutations.tsx +++ b/frontend/src/hooks/api/ldapConfig/mutations.tsx @@ -91,3 +91,48 @@ export const useUpdateLDAPConfig = () => { } }); }; + +export const useCreateLDAPGroupMapping = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + ldapConfigId, + ldapGroupCN, + groupSlug + }: { + ldapConfigId: string; + ldapGroupCN: string; + groupSlug: string; + }) => { + const { data } = await apiRequest.post(`/api/v1/ldap/config/${ldapConfigId}/group-maps`, { + ldapGroupCN, + groupSlug + }); + return data; + }, + onSuccess(_, { ldapConfigId }) { + queryClient.invalidateQueries(ldapConfigKeys.getLDAPGroupMaps(ldapConfigId)); + } + }); +}; + +export const useDeleteLDAPGroupMapping = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + ldapConfigId, + ldapGroupMapId + }: { + ldapConfigId: string; + ldapGroupMapId: string; + }) => { + const { data } = await apiRequest.delete( + `/api/v1/ldap/config/${ldapConfigId}/group-maps/${ldapGroupMapId}` + ); + return data; + }, + onSuccess(_, { ldapConfigId }) { + queryClient.invalidateQueries(ldapConfigKeys.getLDAPGroupMaps(ldapConfigId)); + } + }); +}; diff --git a/frontend/src/hooks/api/ldapConfig/queries.tsx b/frontend/src/hooks/api/ldapConfig/queries.tsx index bc22c62ed..c84f90aac 100644 --- a/frontend/src/hooks/api/ldapConfig/queries.tsx +++ b/frontend/src/hooks/api/ldapConfig/queries.tsx @@ -2,8 +2,11 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { LDAPGroupMap } from "./types"; + export const ldapConfigKeys = { - getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const + getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const, + getLDAPGroupMaps: (ldapConfigId: string) => [{ ldapConfigId }, "ldap-group-maps"] as const }; export const useGetLDAPConfig = (organizationId: string) => { @@ -17,3 +20,19 @@ export const useGetLDAPConfig = (organizationId: string) => { enabled: true }); }; + +export const useGetLDAPGroupMaps = (ldapConfigId: string) => { + return useQuery({ + queryKey: ldapConfigKeys.getLDAPGroupMaps(ldapConfigId), + queryFn: async () => { + if (!ldapConfigId) return []; + + const { data } = await apiRequest.get( + `/api/v1/ldap/config/${ldapConfigId}/group-maps` + ); + + return data; + }, + enabled: true + }); +}; diff --git a/frontend/src/hooks/api/ldapConfig/types.ts b/frontend/src/hooks/api/ldapConfig/types.ts new file mode 100644 index 000000000..d3488d4c8 --- /dev/null +++ b/frontend/src/hooks/api/ldapConfig/types.ts @@ -0,0 +1,6 @@ +export type LDAPGroupMap = { + id: string; + ldapConfigId: string; + ldapGroupCN: string; + groupSlug: string; +}; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPGroupMapModal.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPGroupMapModal.tsx new file mode 100644 index 000000000..6ee72dab2 --- /dev/null +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/LDAPGroupMapModal.tsx @@ -0,0 +1,238 @@ +import { Controller, useForm } from "react-hook-form"; +import { faUsers, faXmark } 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 { + Button, + DeleteActionModal, + EmptyState, + FormControl, + IconButton, + Input, + Modal, + ModalContent, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useCreateLDAPGroupMapping, + useDeleteLDAPGroupMapping, + useGetLDAPConfig, + useGetLDAPGroupMaps} from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z.object({ + ldapGroupCN: z.string().min(1, "LDAP Group CN is required"), + groupSlug: z.string().min(1, "Group Slug is required") +}); + +export type TFormData = z.infer; + +type Props = { + popUp: UsePopUpState<["ldapGroupMap", "deleteLdapGroupMap"]>; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deleteLdapGroupMap"]>, + data?: { + ldapGroupMapId: string; + ldapGroupCN: string; + } + ) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["ldapGroupMap", "deleteLdapGroupMap"]>, + state?: boolean + ) => void; +}; + +export const LDAPGroupMapModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { + const { currentOrg } = useOrganization(); + + const { data: ldapConfig } = useGetLDAPConfig(currentOrg?.id ?? ""); + const { data: groupMaps, isLoading } = useGetLDAPGroupMaps(ldapConfig?.id ?? ""); + const { mutateAsync: createLDAPGroupMapping, isLoading: createIsLoading } = + useCreateLDAPGroupMapping(); + const { mutateAsync: deleteLDAPGroupMapping } = useDeleteLDAPGroupMapping(); + + const { control, handleSubmit, reset } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + ldapGroupCN: "", + groupSlug: "" + } + }); + + const onFormSubmit = async ({ groupSlug, ldapGroupCN }: TFormData) => { + try { + if (!ldapConfig) return; + + await createLDAPGroupMapping({ + ldapConfigId: ldapConfig.id, + groupSlug, + ldapGroupCN + }); + + reset(); + + createNotification({ + text: `Successfully added LDAP group mapping for ${ldapGroupCN}`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to add LDAP group mapping for ${ldapGroupCN}`, + type: "error" + }); + } + }; + + const onDeleteGroupMapSubmit = async ({ + ldapConfigId, + ldapGroupMapId, + ldapGroupCN + }: { + ldapConfigId: string; + ldapGroupMapId: string; + ldapGroupCN: string; + }) => { + try { + await deleteLDAPGroupMapping({ + ldapConfigId, + ldapGroupMapId + }); + + handlePopUpToggle("deleteLdapGroupMap", false); + + createNotification({ + text: `Successfully deleted LDAP group mapping ${ldapGroupCN}`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to delete LDAP group mapping ${ldapGroupCN}`, + type: "error" + }); + } + }; + + return ( + { + handlePopUpToggle("ldapGroupMap", isOpen); + reset(); + }} + > + +

New Group Mapping

+
+
+ ( + + + + )} + /> + ( + +
+ + +
+
+ )} + /> +
+
+

Group Mappings

+ + + + + + + + + + {isLoading && } + {!isLoading && + groupMaps?.map(({ id, ldapGroupCN, groupSlug }) => { + return ( + + + + + + ); + })} + +
LDAP Group CNGroup Slug +
{ldapGroupCN}{groupSlug} + { + handlePopUpOpen("deleteLdapGroupMap", { + ldapGroupMapId: id, + ldapGroupCN + }); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
+ {groupMaps?.length === 0 && ( + + )} +
+ handlePopUpToggle("deleteLdapGroupMap", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const deleteLdapGroupMapData = popUp?.deleteLdapGroupMap?.data as { + ldapGroupMapId: string; + ldapGroupCN: string; + }; + return onDeleteGroupMapSubmit({ + ldapConfigId: ldapConfig?.id ?? "", + ldapGroupMapId: deleteLdapGroupMapData.ldapGroupMapId, + ldapGroupCN: deleteLdapGroupMapData.ldapGroupCN + }); + }} + /> +
+
+ ); +}; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx index 8dacb329e..e2f6e6419 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgLDAPSection.tsx @@ -10,6 +10,7 @@ import { import { useCreateLDAPConfig, useGetLDAPConfig, useUpdateLDAPConfig } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; +import { LDAPGroupMapModal } from "./LDAPGroupMapModal"; import { LDAPModal } from "./LDAPModal"; export const OrgLDAPSection = (): JSX.Element => { @@ -21,6 +22,8 @@ export const OrgLDAPSection = (): JSX.Element => { const { mutateAsync } = useUpdateLDAPConfig(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addLDAP", + "ldapGroupMap", + "deleteLdapGroupMap", "upgradePlan" ] as const); @@ -79,6 +82,16 @@ export const OrgLDAPSection = (): JSX.Element => { } }; + const openLDAPGroupMapModal = () => { + console.log("openLDAPGroupMapModal sub: ", subscription); + if (!subscription?.ldap) { + handlePopUpOpen("upgradePlan"); + return; + } + + handlePopUpOpen("ldapGroupMap"); + }; + return ( <>
@@ -95,6 +108,25 @@ export const OrgLDAPSection = (): JSX.Element => {

Manage LDAP authentication configuration

+
+
+

LDAP Group Mappings

+ + {(isAllowed) => ( + + )} + +
+

+ Manage how LDAP groups are mapped to internal groups in Infisical +

+
{data && (
@@ -122,6 +154,11 @@ export const OrgLDAPSection = (): JSX.Element => { handlePopUpClose={handlePopUpClose} handlePopUpToggle={handlePopUpToggle} /> + handlePopUpToggle("upgradePlan", isOpen)}