From 301b9a46b383fbd88ccca8954787b2d8b16e3659 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 8 Sep 2025 16:16:48 -0700 Subject: [PATCH 1/3] improvement(google-oauth): prevent google oauth enforcement if sso configured, and prevent sso enablement if google oauth enforcement is enabled on both back/frontend and improve labeling --- .../ldap-config/ldap-config-service.ts | 27 +++++++ .../ee/services/oidc/oidc-config-service.ts | 14 ++++ .../saml-config/saml-config-service.ts | 26 +++++++ backend/src/keystore/keystore.ts | 3 +- backend/src/server/routes/index.ts | 1 + backend/src/services/org/org-service.ts | 39 +++++++++++ .../permissions/OrgPermissionCan.tsx | 15 +++- .../OrgSsoTab/OrgGeneralAuthSection.tsx | 70 ++++++++++++++----- .../components/OrgSsoTab/OrgLDAPSection.tsx | 35 +++++++--- .../components/OrgSsoTab/OrgOIDCSection.tsx | 31 ++++++-- .../components/OrgSsoTab/OrgSSOSection.tsx | 31 ++++++-- .../components/OrgSsoTab/OrgSsoTab.tsx | 3 + 12 files changed, 252 insertions(+), 43 deletions(-) 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 a72d50760..4592cf003 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -127,6 +127,20 @@ export const ldapConfigServiceFactory = ({ message: "Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration." }); + + const org = await orgDAL.findOrgById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` }); + } + + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable LDAP SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable LDAP SSO." + }); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId @@ -233,6 +247,19 @@ export const ldapConfigServiceFactory = ({ "Failed to update LDAP configuration due to plan restriction. Upgrade plan to update LDAP configuration." }); + const org = await orgDAL.findOrgById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` }); + } + + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable LDAP SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable LDAP SSO." + }); + } + const updateQuery: TLdapConfigsUpdate = { isActive, url, diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 8f479b12c..fb1e70a85 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -499,6 +499,13 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable OIDC SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable OIDC SSO." + }); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: org.id @@ -586,6 +593,13 @@ export const oidcConfigServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable OIDC SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable OIDC SSO." + }); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: org.id diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 6b8bbe304..1cbe6a930 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -82,6 +82,19 @@ export const samlConfigServiceFactory = ({ "Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration." }); + const org = await orgDAL.findOrgById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` }); + } + + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "You cannot enable SAML SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable SAML SSO." + }); + } + const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId @@ -120,6 +133,19 @@ export const samlConfigServiceFactory = ({ "Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." }); + const org = await orgDAL.findOrgById(orgId); + + if (!org) { + throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` }); + } + + if (org.googleSsoAuthEnforced && isActive) { + throw new BadRequestError({ + message: + "Cannot enable SAML SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable SAML SSO." + }); + } + const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null }; const { encryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 45589e529..a6135e0ca 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -1,9 +1,10 @@ +import { Cluster, Redis } from "ioredis"; + import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; import { applyJitter } from "@app/lib/dates"; import { delay as delayMs } from "@app/lib/delay"; import { ExecutionResult, Redlock, Settings } from "@app/lib/red-lock"; -import { Redis, Cluster } from "ioredis"; export const PgSqlLock = { BootUpMigration: 2023, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 64c9b3389..4803a6cd2 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -807,6 +807,7 @@ export const registerRoutes = async ( groupDAL, orgBotDAL, oidcConfigDAL, + ldapConfigDAL, loginService, projectBotService, reminderService diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 978bc8dae..f99737231 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -14,6 +14,7 @@ import { TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; +import { TLdapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { @@ -125,6 +126,7 @@ type TOrgServiceFactoryDep = { incidentContactDAL: TIncidentContactsDALFactory; samlConfigDAL: Pick; oidcConfigDAL: Pick; + ldapConfigDAL: Pick; smtpService: TSmtpService; tokenService: TAuthTokenServiceFactory; permissionService: TPermissionServiceFactory; @@ -165,6 +167,7 @@ export const orgServiceFactory = ({ projectRoleDAL, samlConfigDAL, oidcConfigDAL, + ldapConfigDAL, projectUserMembershipRoleDAL, identityMetadataDAL, projectBotService, @@ -483,6 +486,42 @@ export const orgServiceFactory = ({ }); } + const samlCfg = await samlConfigDAL.findOne({ + orgId, + isActive: true + }); + + if (samlCfg) { + throw new BadRequestError({ + message: + "Cannot enable Google OAuth enforcement while SAML SSO is configured. Disable SAML SSO to enforce Google OAuth." + }); + } + + const oidcCfg = await oidcConfigDAL.findOne({ + orgId, + isActive: true + }); + + if (oidcCfg) { + throw new BadRequestError({ + message: + "Cannot enable Google OAuth enforcement while OIDC SSO is configured. Disable OIDC SSO to enforce Google OAuth." + }); + } + + const ldapCfg = await ldapConfigDAL.findOne({ + orgId, + isActive: true + }); + + if (ldapCfg) { + throw new BadRequestError({ + message: + "Cannot enable Google OAuth enforcement while LDAP SSO is configured. Disable LDAP SSO to enforce Google OAuth." + }); + } + if (!currentOrg.googleSsoAuthLastUsed) { throw new BadRequestError({ message: diff --git a/frontend/src/components/permissions/OrgPermissionCan.tsx b/frontend/src/components/permissions/OrgPermissionCan.tsx index d2e698e88..8e0bf08ad 100644 --- a/frontend/src/components/permissions/OrgPermissionCan.tsx +++ b/frontend/src/components/permissions/OrgPermissionCan.tsx @@ -1,6 +1,7 @@ import { FunctionComponent, ReactNode } from "react"; import { BoundCanProps, Can } from "@casl/react"; +import { TooltipProps } from "@app/components/v2/Tooltip/Tooltip"; import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext"; import { AccessRestrictedBanner, Tooltip } from "../v2"; @@ -20,6 +21,7 @@ type Props = { renderTooltip?: boolean; allowedLabel?: string; renderGuardBanner?: boolean; + tooltipProps?: Omit; } & BoundCanProps; export const OrgPermissionCan: FunctionComponent = ({ @@ -29,6 +31,7 @@ export const OrgPermissionCan: FunctionComponent = ({ renderTooltip, allowedLabel, renderGuardBanner, + tooltipProps, ...props }) => { const { permission } = useOrgPermission(); @@ -43,11 +46,19 @@ export const OrgPermissionCan: FunctionComponent = ({ : children; if (!isAllowed && passThrough) { - return {finalChild}; + return ( + + {finalChild} + + ); } if (isAllowed && renderTooltip && allowedLabel) { - return {finalChild}; + return ( + + {finalChild} + + ); } if (!isAllowed && renderGuardBanner) { diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx index b9ca11748..c3d8cea98 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx @@ -24,11 +24,17 @@ enum EnforceAuthType { export const OrgGeneralAuthSection = ({ isSamlConfigured, isOidcConfigured, - isGoogleConfigured + isGoogleConfigured, + isSamlActive, + isOidcActive, + isLdapActive }: { isSamlConfigured: boolean; isOidcConfigured: boolean; isGoogleConfigured: boolean; + isSamlActive: boolean; + isOidcActive: boolean; + isLdapActive: boolean; }) => { const { currentOrg } = useOrganization(); const { subscription } = useSubscription(); @@ -126,6 +132,15 @@ export const OrgGeneralAuthSection = ({ } }; + const isGoogleOAuthEnforced = currentOrg.googleSsoAuthEnforced; + + const getActiveSsoLabel = () => { + if (isSamlActive) return "SAML"; + if (isOidcActive) return "OIDC"; + if (isLdapActive) return "LDAP"; + return ""; + }; + return (
@@ -135,7 +150,7 @@ export const OrgGeneralAuthSection = ({

-
+
Enforce SAML SSO @@ -160,7 +175,7 @@ export const OrgGeneralAuthSection = ({

-
+
Enforce OIDC SSO @@ -188,26 +203,47 @@ export const OrgGeneralAuthSection = ({
- Enforce Google SSO + Enforce Google OAuth
- + {(isAllowed) => ( - - handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE) - } - isChecked={currentOrg?.googleSsoAuthEnforced ?? false} - isDisabled={!isAllowed || currentOrg?.authEnforced} - /> +
+ + handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE) + } + isChecked={currentOrg?.googleSsoAuthEnforced ?? false} + isDisabled={ + !isAllowed || + currentOrg?.authEnforced || + isOidcActive || + isSamlActive || + isLdapActive + } + /> +
)}

- Enforce users to authenticate via Google OAuth SSO to access this organization. + Enforce users to authenticate via Google OAuth to access this organization.
When this is enabled your organization members will only be able to login with Google - SSO (not Google SAML). + OAuth (not Google SAML).

@@ -267,8 +303,8 @@ export const OrgGeneralAuthSection = ({

- Allow organization admins to bypass SAML enforcement when SSO is unavailable, - misconfigured, or inaccessible. + Allow organization admins to bypass SSO login enforcement when your SSO provider is + unavailable, misconfigured, or inaccessible.

diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx index e66987bae..45392a660 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx @@ -94,6 +94,8 @@ export const OrgLDAPSection = (): JSX.Element => { handlePopUpOpen("ldapGroupMap"); }; + const isGoogleOAuthEnabled = currentOrg.googleSsoAuthEnforced; + return (
@@ -116,16 +118,31 @@ export const OrgLDAPSection = (): JSX.Element => {

Enable LDAP

- + {(isAllowed) => ( - handleLDAPToggle(value)} - isChecked={data ? data.isActive : false} - isDisabled={!isAllowed} - > - Enable - +
+ handleLDAPToggle(value)} + isChecked={data ? data.isActive : false} + isDisabled={!isAllowed || isGoogleOAuthEnabled} + > + Enable + +
)}
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx index ce10c3907..3ba1a8275 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgOIDCSection.tsx @@ -83,6 +83,8 @@ export const OrgOIDCSection = (): JSX.Element => { } }; + const isGoogleOAuthEnabled = currentOrg.googleSsoAuthEnforced; + return (
@@ -106,14 +108,29 @@ export const OrgOIDCSection = (): JSX.Element => {

Enable OIDC

{!isPending && ( - + {(isAllowed) => ( - handleOIDCToggle(value)} - isChecked={data ? data.isActive : false} - isDisabled={!isAllowed} - /> +
+ handleOIDCToggle(value)} + isChecked={data ? data.isActive : false} + isDisabled={!isAllowed || isGoogleOAuthEnabled} + /> +
)}
)} diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx index 33843f50f..53e6cece7 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSSOSection.tsx @@ -78,6 +78,8 @@ export const OrgSSOSection = (): JSX.Element => { } }; + const isGoogleOAuthEnabled = currentOrg.googleSsoAuthEnforced; + return (
@@ -99,14 +101,29 @@ export const OrgSSOSection = (): JSX.Element => {

Enable SAML

{!isPending && ( - + {(isAllowed) => ( - handleSamlSSOToggle(value)} - isChecked={data ? data.isActive : false} - isDisabled={!isAllowed} - /> +
+ handleSamlSSOToggle(value)} + isChecked={data ? data.isActive : false} + isDisabled={!isAllowed || isGoogleOAuthEnabled} + /> +
)}
)} diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx index 9964fdf4d..e4e3a9c23 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgSsoTab.tsx @@ -184,6 +184,9 @@ export const OrgSsoTab = withPermission( isSamlConfigured={isSamlConfigured} isOidcConfigured={isOidcConfigured} isGoogleConfigured={isGoogleConfigured} + isSamlActive={Boolean(samlConfig?.isActive)} + isOidcActive={Boolean(oidcConfig?.isActive)} + isLdapActive={Boolean(ldapConfig?.isActive)} /> )} From 9ad8bc697714aaff400d0aed8a165089eb7917eb Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 8 Sep 2025 16:26:14 -0700 Subject: [PATCH 2/3] improvement: correct switch enable switch id --- .../SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx index 45392a660..a17483fdd 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgSsoTab/OrgLDAPSection.tsx @@ -135,7 +135,7 @@ export const OrgLDAPSection = (): JSX.Element => { {(isAllowed) => (
handleLDAPToggle(value)} isChecked={data ? data.isActive : false} isDisabled={!isAllowed || isGoogleOAuthEnabled} From b113eabeb70ba4a56511931d99e0b0a89dbccab3 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 9 Sep 2025 10:27:41 -0700 Subject: [PATCH 3/3] improvement: address feedback --- backend/src/services/org/org-service.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index f99737231..ffcf4459e 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -9,8 +9,10 @@ import { ProjectMembershipRole, ProjectVersion, TableName, + TOidcConfigs, TProjectMemberships, TProjectUserMembershipRolesInsert, + TSamlConfigs, TUsers } from "@app/db/schemas"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; @@ -449,16 +451,20 @@ export const orgServiceFactory = ({ }); } - if (authEnforced) { - const samlCfg = await samlConfigDAL.findOne({ + let samlCfg: TSamlConfigs | undefined; + let oidcCfg: TOidcConfigs | undefined; + if (authEnforced || googleSsoAuthEnforced) { + samlCfg = await samlConfigDAL.findOne({ orgId, isActive: true }); - const oidcCfg = await oidcConfigDAL.findOne({ + oidcCfg = await oidcConfigDAL.findOne({ orgId, isActive: true }); + } + if (authEnforced) { if (!samlCfg && !oidcCfg) throw new NotFoundError({ message: `SAML or OIDC configuration for organization with ID '${orgId}' not found` @@ -486,11 +492,6 @@ export const orgServiceFactory = ({ }); } - const samlCfg = await samlConfigDAL.findOne({ - orgId, - isActive: true - }); - if (samlCfg) { throw new BadRequestError({ message: @@ -498,11 +499,6 @@ export const orgServiceFactory = ({ }); } - const oidcCfg = await oidcConfigDAL.findOne({ - orgId, - isActive: true - }); - if (oidcCfg) { throw new BadRequestError({ message: