diff --git a/backend/src/ee/routes/v1/scim-router.ts b/backend/src/ee/routes/v1/scim-router.ts index 6116599f1..095815212 100644 --- a/backend/src/ee/routes/v1/scim-router.ts +++ b/backend/src/ee/routes/v1/scim-router.ts @@ -9,7 +9,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { try { const strBody = body instanceof Buffer ? body.toString() : body; - const json: unknown = JSON.parse(strBody); // TODO: update + const json: unknown = JSON.parse(strBody); done(null, json); } catch (err) { const error = err as Error; @@ -294,13 +294,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { familyName: z.string().trim(), givenName: z.string().trim() }), - // emails: z.array( - // z.object({ - // primary: z.boolean(), - // value: z.string().email(), - // type: z.string().trim() - // }) - // ), displayName: z.string().trim(), active: z.boolean() }), @@ -335,20 +328,4 @@ export const registerScimRouter = async (server: FastifyZodProvider) => { return user; } }); - - // server.route({ - // url: "/Users/:userId", - // method: "DELETE", - // schema: { - // body: z.object({}), - // response: { - // 200: z.object({}) - // } - // }, - // onRequest: verifyAuth([AuthMode.SCIM_TOKEN]), - // handler: () => { - // // TODO: update a user's profile - // return {}; - // } - // }); }; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index b6f6d9769..8535380de 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -9,6 +9,8 @@ import { TOrgPermission } from "@app/lib/types"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { TOrgDALFactory } from "@app/services/org/org-dal"; import { deleteOrgMembership } from "@app/services/org/org-fns"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -29,10 +31,14 @@ import { } from "./scim-types"; type TScimServiceFactoryDep = { - // TODO: pick types - scimDAL: TScimDALFactory; // TODO: pick - userDAL: TUserDALFactory; // TODO: pick - orgDAL: TOrgDALFactory; // TODO: pick + scimDAL: Pick; + userDAL: Pick; + orgDAL: Pick< + TOrgDALFactory, + "createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" + >; + projectDAL: Pick; + projectMembershipDAL: Pick; licenseService: Pick; permissionService: Pick; smtpService: TSmtpService; @@ -45,6 +51,8 @@ export const scimServiceFactory = ({ scimDAL, userDAL, orgDAL, + projectDAL, + projectMembershipDAL, permissionService, smtpService }: TScimServiceFactoryDep) => { @@ -325,7 +333,9 @@ export const scimServiceFactory = ({ await deleteOrgMembership({ orgMembershipId: membership.id, orgId: membership.orgId, - orgDAL + orgDAL, + projectDAL, + projectMembershipDAL }); } @@ -368,7 +378,9 @@ export const scimServiceFactory = ({ await deleteOrgMembership({ orgMembershipId: membership.id, orgId: membership.orgId, - orgDAL + orgDAL, + projectDAL, + projectMembershipDAL }); } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e55704748..741e2b703 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -219,6 +219,8 @@ export const registerRoutes = async ( scimDAL, userDAL, orgDAL, + projectDAL, + projectMembershipDAL, permissionService, smtpService }); diff --git a/backend/src/services/org/org-fns.ts b/backend/src/services/org/org-fns.ts index 9ad4d5ddb..ec6d4cb2d 100644 --- a/backend/src/services/org/org-fns.ts +++ b/backend/src/services/org/org-fns.ts @@ -1,21 +1,41 @@ import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; type TDeleteOrgMembership = { orgMembershipId: string; orgId: string; - orgDAL: TOrgDALFactory; + orgDAL: Pick; + projectDAL: Pick; + projectMembershipDAL: Pick; }; -export const deleteOrgMembership = async ({ orgMembershipId, orgId, orgDAL }: TDeleteOrgMembership) => { - // TODO: improve this implementation +export const deleteOrgMembership = async ({ + orgMembershipId, + orgId, + orgDAL, + projectDAL, + projectMembershipDAL +}: TDeleteOrgMembership) => { + const membership = await orgDAL.transaction(async (tx) => { + // delete org membership + const orgMembership = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx); - // delete - const m2 = await orgDAL.transaction(async (tx) => { - const m1 = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx); - // const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx); - // delete project memberships - return m1; + const projects = await projectDAL.find({ orgId }, { tx }); + + // delete associated project memberships + await projectMembershipDAL.delete( + { + $in: { + projectId: projects.map((project) => project.id) + }, + userId: orgMembership.userId as string + }, + tx + ); + + return orgMembership; }); - return m2; + return membership; }; diff --git a/frontend/src/hooks/api/ssoConfig/queries.tsx b/frontend/src/hooks/api/ssoConfig/queries.tsx index 9b75095b8..c294679f8 100644 --- a/frontend/src/hooks/api/ssoConfig/queries.tsx +++ b/frontend/src/hooks/api/ssoConfig/queries.tsx @@ -1,6 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { organizationKeys } from "@app/hooks/api/organization/queries"; const ssoConfigKeys = { getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const @@ -82,8 +83,12 @@ export const useUpdateSSOConfig = () => { return data; }, - onSuccess(_, dto) { - queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId)); + onSuccess(_, { organizationId, isActive }) { + if (isActive === false) { + queryClient.invalidateQueries(organizationKeys.getUserOrganizations); + } + + queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(organizationId)); } }); }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx index 4706c94c9..406bd569e 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx @@ -81,6 +81,12 @@ const SIMPLE_PERMISSION_OPTIONS = [ subtitle: "Define organization level SSO requirements", icon: faSignIn, formName: "sso" + }, + { + title: "SCIM", + subtitle: "Define organization level SCIM requirements", + icon: faUsers, + formName: "scim" } ] as const; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts index 3590b2e20..415fa68df 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts @@ -34,6 +34,7 @@ export const formSchema = z.object({ "incident-contact": generalPermissionSchema, "secret-scanning": generalPermissionSchema, sso: generalPermissionSchema, + scim: generalPermissionSchema, billing: generalPermissionSchema, identity: generalPermissionSchema }) diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx index 9e2b6f085..ef81a6cab 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx @@ -10,14 +10,14 @@ import { useOrganization, useSubscription } from "@app/context"; -import { usePopUp } from "@app/hooks/usePopUp"; import { useLogoutUser, useUpdateOrg } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; export const OrgGeneralAuthSection = () => { const { createNotification } = useNotificationContext(); const { currentOrg } = useOrganization(); const { subscription } = useSubscription(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ "upgradePlan" ] as const); @@ -52,7 +52,7 @@ export const OrgGeneralAuthSection = () => { } catch (err) { console.error(err); createNotification({ - text: `Failed to ${value ? "enforce" : "un-enforce"} org-level auth`, + text: (err as { response: { data: { message: string; }}}).response.data.message, type: "error" }); } diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx index ea496748b..041518d9f 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgAuthTab/OrgSCIMSection.tsx @@ -11,12 +11,12 @@ import { import { OrgPermissionActions, OrgPermissionSubjects, - useSubscription, - useOrganization -} from "@app/context"; -import { usePopUp } from "@app/hooks/usePopUp"; -import { ScimTokenModal } from "./ScimTokenModal"; + useOrganization, + useSubscription} from "@app/context"; import { useUpdateOrg } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { ScimTokenModal } from "./ScimTokenModal"; export const OrgScimSection = () => { const { createNotification } = useNotificationContext(); @@ -56,9 +56,8 @@ export const OrgScimSection = () => { type: "success" }); } catch (err) { - console.error(err); createNotification({ - text: `Failed to ${value ? "enable" : "disable"} SCIM provisioning`, + text: (err as { response: { data: { message: string; }}}).response.data.message, type: "error" }); } @@ -82,20 +81,22 @@ export const OrgScimSection = () => { - { - if (subscription?.scim) { - handleEnableSCIMToggle(value) - } else { - handlePopUpOpen("upgradePlan"); - } - }} - isChecked={currentOrg?.scimEnabled ?? false} - isDisabled={false} - > - Enable SCIM Provisioning - + {(isAllowed) => ( + { + if (subscription?.scim) { + handleEnableSCIMToggle(value) + } else { + handlePopUpOpen("upgradePlan"); + } + }} + isChecked={currentOrg?.scimEnabled ?? false} + isDisabled={!isAllowed} + > + Enable SCIM Provisioning + + )}