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
+1 -24
View File
@@ -9,7 +9,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
try { try {
const strBody = body instanceof Buffer ? body.toString() : body; 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); done(null, json);
} catch (err) { } catch (err) {
const error = err as Error; const error = err as Error;
@@ -294,13 +294,6 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
familyName: z.string().trim(), familyName: z.string().trim(),
givenName: 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(), displayName: z.string().trim(),
active: z.boolean() active: z.boolean()
}), }),
@@ -335,20 +328,4 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
return user; 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 {};
// }
// });
}; };
+18 -6
View File
@@ -9,6 +9,8 @@ import { TOrgPermission } from "@app/lib/types";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { deleteOrgMembership } from "@app/services/org/org-fns"; 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 { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -29,10 +31,14 @@ import {
} from "./scim-types"; } from "./scim-types";
type TScimServiceFactoryDep = { type TScimServiceFactoryDep = {
// TODO: pick types scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
scimDAL: TScimDALFactory; // TODO: pick userDAL: Pick<TUserDALFactory, "findOne" | "create" | "transaction">;
userDAL: TUserDALFactory; // TODO: pick orgDAL: Pick<
orgDAL: TOrgDALFactory; // TODO: pick TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction"
>;
projectDAL: Pick<TProjectDALFactory, "find">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: TSmtpService; smtpService: TSmtpService;
@@ -45,6 +51,8 @@ export const scimServiceFactory = ({
scimDAL, scimDAL,
userDAL, userDAL,
orgDAL, orgDAL,
projectDAL,
projectMembershipDAL,
permissionService, permissionService,
smtpService smtpService
}: TScimServiceFactoryDep) => { }: TScimServiceFactoryDep) => {
@@ -325,7 +333,9 @@ export const scimServiceFactory = ({
await deleteOrgMembership({ await deleteOrgMembership({
orgMembershipId: membership.id, orgMembershipId: membership.id,
orgId: membership.orgId, orgId: membership.orgId,
orgDAL orgDAL,
projectDAL,
projectMembershipDAL
}); });
} }
@@ -368,7 +378,9 @@ export const scimServiceFactory = ({
await deleteOrgMembership({ await deleteOrgMembership({
orgMembershipId: membership.id, orgMembershipId: membership.id,
orgId: membership.orgId, orgId: membership.orgId,
orgDAL orgDAL,
projectDAL,
projectMembershipDAL
}); });
} }
+2
View File
@@ -219,6 +219,8 @@ export const registerRoutes = async (
scimDAL, scimDAL,
userDAL, userDAL,
orgDAL, orgDAL,
projectDAL,
projectMembershipDAL,
permissionService, permissionService,
smtpService smtpService
}); });
+30 -10
View File
@@ -1,21 +1,41 @@
import { TOrgDALFactory } from "@app/services/org/org-dal"; 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 = { type TDeleteOrgMembership = {
orgMembershipId: string; orgMembershipId: string;
orgId: 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) => { export const deleteOrgMembership = async ({
// TODO: improve this implementation 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 projects = await projectDAL.find({ orgId }, { tx });
const m2 = await orgDAL.transaction(async (tx) => {
const m1 = await orgDAL.deleteMembershipById(orgMembershipId, orgId, tx); // delete associated project memberships
// const [deletedMembership] = await projectMembershipDAL.delete({ projectId, id: membershipId }, tx); await projectMembershipDAL.delete(
// delete project memberships {
return m1; $in: {
projectId: projects.map((project) => project.id)
},
userId: orgMembership.userId as string
},
tx
);
return orgMembership;
}); });
return m2; return membership;
}; };
+7 -2
View File
@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { organizationKeys } from "@app/hooks/api/organization/queries";
const ssoConfigKeys = { const ssoConfigKeys = {
getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const
@@ -82,8 +83,12 @@ export const useUpdateSSOConfig = () => {
return data; return data;
}, },
onSuccess(_, dto) { onSuccess(_, { organizationId, isActive }) {
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId)); if (isActive === false) {
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
}
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(organizationId));
} }
}); });
}; };
@@ -81,6 +81,12 @@ const SIMPLE_PERMISSION_OPTIONS = [
subtitle: "Define organization level SSO requirements", subtitle: "Define organization level SSO requirements",
icon: faSignIn, icon: faSignIn,
formName: "sso" formName: "sso"
},
{
title: "SCIM",
subtitle: "Define organization level SCIM requirements",
icon: faUsers,
formName: "scim"
} }
] as const; ] as const;
@@ -34,6 +34,7 @@ export const formSchema = z.object({
"incident-contact": generalPermissionSchema, "incident-contact": generalPermissionSchema,
"secret-scanning": generalPermissionSchema, "secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema, sso: generalPermissionSchema,
scim: generalPermissionSchema,
billing: generalPermissionSchema, billing: generalPermissionSchema,
identity: generalPermissionSchema identity: generalPermissionSchema
}) })
@@ -10,14 +10,14 @@ import {
useOrganization, useOrganization,
useSubscription useSubscription
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp";
import { useLogoutUser, useUpdateOrg } from "@app/hooks/api"; import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
export const OrgGeneralAuthSection = () => { export const OrgGeneralAuthSection = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan" "upgradePlan"
] as const); ] as const);
@@ -52,7 +52,7 @@ export const OrgGeneralAuthSection = () => {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createNotification({ createNotification({
text: `Failed to ${value ? "enforce" : "un-enforce"} org-level auth`, text: (err as { response: { data: { message: string; }}}).response.data.message,
type: "error" type: "error"
}); });
} }
@@ -11,12 +11,12 @@ import {
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
useSubscription, useOrganization,
useOrganization useSubscription} from "@app/context";
} from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp";
import { ScimTokenModal } from "./ScimTokenModal";
import { useUpdateOrg } from "@app/hooks/api"; import { useUpdateOrg } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { ScimTokenModal } from "./ScimTokenModal";
export const OrgScimSection = () => { export const OrgScimSection = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -56,9 +56,8 @@ export const OrgScimSection = () => {
type: "success" type: "success"
}); });
} catch (err) { } catch (err) {
console.error(err);
createNotification({ createNotification({
text: `Failed to ${value ? "enable" : "disable"} SCIM provisioning`, text: (err as { response: { data: { message: string; }}}).response.data.message,
type: "error" type: "error"
}); });
} }
@@ -82,20 +81,22 @@ export const OrgScimSection = () => {
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Scim}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Scim}>
<Switch {(isAllowed) => (
id="enable-scim" <Switch
onCheckedChange={(value) => { id="enable-scim"
if (subscription?.scim) { onCheckedChange={(value) => {
handleEnableSCIMToggle(value) if (subscription?.scim) {
} else { handleEnableSCIMToggle(value)
handlePopUpOpen("upgradePlan"); } else {
} handlePopUpOpen("upgradePlan");
}} }
isChecked={currentOrg?.scimEnabled ?? false} }}
isDisabled={false} isChecked={currentOrg?.scimEnabled ?? false}
> isDisabled={!isAllowed}
Enable SCIM Provisioning >
</Switch> Enable SCIM Provisioning
</Switch>
)}
</OrgPermissionCan> </OrgPermissionCan>
<ScimTokenModal <ScimTokenModal
popUp={popUp} popUp={popUp}