Minor UX adjustments to SCIM

This commit is contained in:
Tuan Dang
2024-02-19 12:25:25 -08:00
parent 7a65f8c837
commit 6f9b30b46e
9 changed files with 90 additions and 66 deletions

View File

@@ -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 {};
// }
// });
};

View File

@@ -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<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<TUserDALFactory, "findOne" | "create" | "transaction">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
>;
projectDAL: Pick<TProjectDALFactory, "find">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
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
});
}

View File

@@ -219,6 +219,8 @@ export const registerRoutes = async (
scimDAL,
userDAL,
orgDAL,
projectDAL,
projectMembershipDAL,
permissionService,
smtpService
});

View File

@@ -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<TOrgDALFactory, "findMembership" | "deleteMembershipById" | "transaction">;
projectDAL: Pick<TProjectDALFactory, "find">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
};
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;
};

View File

@@ -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));
}
});
};

View File

@@ -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;

View File

@@ -34,6 +34,7 @@ export const formSchema = z.object({
"incident-contact": generalPermissionSchema,
"secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema,
scim: generalPermissionSchema,
billing: generalPermissionSchema,
identity: generalPermissionSchema
})

View File

@@ -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"
});
}

View File

@@ -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 = () => {
</OrgPermissionCan>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Scim}>
<Switch
id="enable-scim"
onCheckedChange={(value) => {
if (subscription?.scim) {
handleEnableSCIMToggle(value)
} else {
handlePopUpOpen("upgradePlan");
}
}}
isChecked={currentOrg?.scimEnabled ?? false}
isDisabled={false}
>
Enable SCIM Provisioning
</Switch>
{(isAllowed) => (
<Switch
id="enable-scim"
onCheckedChange={(value) => {
if (subscription?.scim) {
handleEnableSCIMToggle(value)
} else {
handlePopUpOpen("upgradePlan");
}
}}
isChecked={currentOrg?.scimEnabled ?? false}
isDisabled={!isAllowed}
>
Enable SCIM Provisioning
</Switch>
)}
</OrgPermissionCan>
<ScimTokenModal
popUp={popUp}