From d0db5c00e8f731eb072dc453bb4896b869af9033 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 16 Apr 2025 01:35:39 +0800 Subject: [PATCH] misc: allow org admins to bypass sso enforcement --- .../src/ee/services/permission/permission-dal.ts | 9 +++++++++ .../src/ee/services/permission/permission-fns.ts | 9 +++++++-- .../ee/services/permission/permission-service.ts | 4 ++-- .../src/server/routes/v1/organization-router.ts | 3 ++- backend/src/services/org/org-dal.ts | 7 +++++-- frontend/src/helpers/roles.ts | 2 +- frontend/src/hooks/api/organization/types.ts | 1 + .../components/InitialStep/InitialStep.tsx | 15 ++++++++++++++- .../pages/auth/SelectOrgPage/SelectOrgPage.tsx | 3 ++- .../OrgAuthTab/OrgGeneralAuthSection.tsx | 2 +- .../components/OrgAuthTab/OrgOIDCSection.tsx | 2 +- 11 files changed, 45 insertions(+), 12 deletions(-) diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index 9b23d6113..b76403400 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { TDbClient } from "@app/db"; import { IdentityProjectMembershipRoleSchema, + OrgMembershipRole, OrgMembershipsSchema, TableName, TProjectRoles, @@ -571,6 +572,11 @@ export const permissionDALFactory = (db: TDbClient) => { }) .join(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId])) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) + .join(TableName.OrgMembership, (qb) => { + void qb + .on(`${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .andOn(`${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`); + }) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder .on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`) @@ -670,6 +676,7 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"), db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), + db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"), db.ref("orgId").withSchema(TableName.Project), db.ref("type").withSchema(TableName.Project).as("projectType"), db.ref("id").withSchema(TableName.Project).as("projectId"), @@ -683,6 +690,7 @@ export const permissionDALFactory = (db: TDbClient) => { orgId, username, orgAuthEnforced, + orgRole, membershipId, groupMembershipId, membershipCreatedAt, @@ -694,6 +702,7 @@ export const permissionDALFactory = (db: TDbClient) => { }) => ({ orgId, orgAuthEnforced, + orgRole: orgRole as OrgMembershipRole, userId, projectId, username, diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 88bace1f0..410fc5819 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability"; import { z } from "zod"; -import { TOrganizations } from "@app/db/schemas"; +import { OrgMembershipRole, TOrganizations } from "@app/db/schemas"; import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; @@ -118,12 +118,17 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { ].includes(actorAuthMethod); } -function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) { +function validateOrgSSO( + actorAuthMethod: ActorAuthMethod, + isOrgSsoEnforced: TOrganizations["authEnforced"], + orgRole: OrgMembershipRole +) { if (actorAuthMethod === undefined) { throw new UnauthorizedError({ name: "No auth method defined" }); } if ( + orgRole !== OrgMembershipRole.Admin && isOrgSsoEnforced && actorAuthMethod !== null && !isAuthMethodSaml(actorAuthMethod) && diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 299f509d7..0a63931aa 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -139,7 +139,7 @@ export const permissionServiceFactory = ({ throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); } - validateOrgSSO(authMethod, membership.orgAuthEnforced); + validateOrgSSO(authMethod, membership.orgAuthEnforced, membership.role as OrgMembershipRole); const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat( membership?.groups?.map(({ role, customRolePermission }) => ({ @@ -226,7 +226,7 @@ export const permissionServiceFactory = ({ throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); } - validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced); + validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced, userProjectPermission.orgRole); if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) { throw new BadRequestError({ diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 93376d261..c6391d4f7 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -31,7 +31,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { 200: z.object({ organizations: sanitizedOrganizationSchema .extend({ - orgAuthMethod: z.string() + orgAuthMethod: z.string(), + userRole: z.string() }) .array() }) diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 85d348854..02bf58321 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -96,7 +96,9 @@ export const orgDALFactory = (db: TDbClient) => { }; // special query - const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => { + const findAllOrgsByUserId = async ( + userId: string + ): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => { try { const org = (await db .replicaNode()(TableName.OrgMembership) @@ -117,6 +119,7 @@ export const orgDALFactory = (db: TDbClient) => { ); }) .select(selectAllTableCols(TableName.Organization)) + .select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole")) .select( db.raw(` CASE @@ -125,7 +128,7 @@ export const orgDALFactory = (db: TDbClient) => { ELSE '' END as "orgAuthMethod" `) - )) as (TOrganizations & { orgAuthMethod: string })[]; + )) as (TOrganizations & { orgAuthMethod: string; userRole: string })[]; return org; } catch (error) { diff --git a/frontend/src/helpers/roles.ts b/frontend/src/helpers/roles.ts index 4e26e1b15..dcf80d452 100644 --- a/frontend/src/helpers/roles.ts +++ b/frontend/src/helpers/roles.ts @@ -1,6 +1,6 @@ import { ProjectMembershipRole, TOrgRole } from "@app/hooks/api/roles/types"; -enum OrgMembershipRole { +export enum OrgMembershipRole { Admin = "admin", Member = "member", NoAccess = "no-access" diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index c563d8c43..6eeebd819 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -17,6 +17,7 @@ export type Organization = { selectedMfaMethod?: MfaMethod; shouldUseNewPrivilegeSystem: boolean; allowSecretSharingOutsideOrganization?: boolean; + userRole: string; }; export type UpdateOrgDTO = { diff --git a/frontend/src/pages/auth/LoginPage/components/InitialStep/InitialStep.tsx b/frontend/src/pages/auth/LoginPage/components/InitialStep/InitialStep.tsx index 16de4c7ec..f9ba1bf98 100644 --- a/frontend/src/pages/auth/LoginPage/components/InitialStep/InitialStep.tsx +++ b/frontend/src/pages/auth/LoginPage/components/InitialStep/InitialStep.tsx @@ -34,6 +34,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); const [loginError, setLoginError] = useState(false); + const [isOtherLoginMethodsSelected, setIsOtherLoginMethodsSelected] = useState(false); const { config } = useServerConfig(); const queryParams = new URLSearchParams(window.location.search); const [captchaToken, setCaptchaToken] = useState(""); @@ -160,7 +161,11 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: setIsLoading(false); }; - if (config.defaultAuthOrgAuthEnforced && config.defaultAuthOrgAuthMethod) { + if ( + config.defaultAuthOrgAuthEnforced && + config.defaultAuthOrgAuthMethod && + !isOtherLoginMethodsSelected + ) { return (
)} +
); } diff --git a/frontend/src/pages/auth/SelectOrgPage/SelectOrgPage.tsx b/frontend/src/pages/auth/SelectOrgPage/SelectOrgPage.tsx index 76d80b805..7dbf8d457 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgPage.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgPage.tsx @@ -14,6 +14,7 @@ import { IsCliLoginSuccessful } from "@app/components/utilities/attemptCliLogin" import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, Spinner } from "@app/components/v2"; import { SessionStorageKeys } from "@app/const"; +import { OrgMembershipRole } from "@app/helpers/roles"; import { useToggle } from "@app/hooks"; import { useGetOrganizations, @@ -68,7 +69,7 @@ export const SelectOrganizationPage = () => { const handleSelectOrganization = useCallback( async (organization: Organization) => { - if (organization.authEnforced) { + if (organization.authEnforced && organization.userRole !== OrgMembershipRole.Admin) { // org has an org-level auth method enabled (e.g. SAML) // -> logout + redirect to SAML SSO await logout.mutateAsync(); diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx index 5e04f173e..6bcdef231 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgGeneralAuthSection.tsx @@ -85,7 +85,7 @@ export const OrgGeneralAuthSection = () => {

- Enforce members to authenticate via SAML to access this organization + Enforce non-admin users to authenticate via SAML to access this organization

{

- Enforce members to authenticate via OIDC to access this organization + Enforce non-admin users to authenticate via OIDC to access this organization.