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

This commit is contained in:
Scott Wilson
2025-09-08 16:16:48 -07:00
parent 56ad42a305
commit 301b9a46b3
12 changed files with 252 additions and 43 deletions
@@ -127,6 +127,20 @@ export const ldapConfigServiceFactory = ({
message: message:
"Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration." "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({ const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId orgId
@@ -233,6 +247,19 @@ export const ldapConfigServiceFactory = ({
"Failed to update LDAP configuration due to plan restriction. Upgrade plan to update LDAP configuration." "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 = { const updateQuery: TLdapConfigsUpdate = {
isActive, isActive,
url, url,
@@ -499,6 +499,13 @@ export const oidcConfigServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso); 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({ const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: org.id orgId: org.id
@@ -586,6 +593,13 @@ export const oidcConfigServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso); 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({ const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: org.id orgId: org.id
@@ -82,6 +82,19 @@ export const samlConfigServiceFactory = ({
"Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration." "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({ const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId orgId
@@ -120,6 +133,19 @@ export const samlConfigServiceFactory = ({
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration." "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 updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null };
const { encryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
+2 -1
View File
@@ -1,9 +1,10 @@
import { Cluster, Redis } from "ioredis";
import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis"; import { buildRedisFromConfig, TRedisConfigKeys } from "@app/lib/config/redis";
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext"; import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
import { applyJitter } from "@app/lib/dates"; import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay"; import { delay as delayMs } from "@app/lib/delay";
import { ExecutionResult, Redlock, Settings } from "@app/lib/red-lock"; import { ExecutionResult, Redlock, Settings } from "@app/lib/red-lock";
import { Redis, Cluster } from "ioredis";
export const PgSqlLock = { export const PgSqlLock = {
BootUpMigration: 2023, BootUpMigration: 2023,
+1
View File
@@ -807,6 +807,7 @@ export const registerRoutes = async (
groupDAL, groupDAL,
orgBotDAL, orgBotDAL,
oidcConfigDAL, oidcConfigDAL,
ldapConfigDAL,
loginService, loginService,
projectBotService, projectBotService,
reminderService reminderService
+39
View File
@@ -14,6 +14,7 @@ import {
TUsers TUsers
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; 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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { import {
@@ -125,6 +126,7 @@ type TOrgServiceFactoryDep = {
incidentContactDAL: TIncidentContactsDALFactory; incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">; samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">; oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
ldapConfigDAL: Pick<TLdapConfigDALFactory, "findOne">;
smtpService: TSmtpService; smtpService: TSmtpService;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
@@ -165,6 +167,7 @@ export const orgServiceFactory = ({
projectRoleDAL, projectRoleDAL,
samlConfigDAL, samlConfigDAL,
oidcConfigDAL, oidcConfigDAL,
ldapConfigDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityMetadataDAL, identityMetadataDAL,
projectBotService, 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) { if (!currentOrg.googleSsoAuthLastUsed) {
throw new BadRequestError({ throw new BadRequestError({
message: message:
@@ -1,6 +1,7 @@
import { FunctionComponent, ReactNode } from "react"; import { FunctionComponent, ReactNode } from "react";
import { BoundCanProps, Can } from "@casl/react"; import { BoundCanProps, Can } from "@casl/react";
import { TooltipProps } from "@app/components/v2/Tooltip/Tooltip";
import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext"; import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
import { AccessRestrictedBanner, Tooltip } from "../v2"; import { AccessRestrictedBanner, Tooltip } from "../v2";
@@ -20,6 +21,7 @@ type Props = {
renderTooltip?: boolean; renderTooltip?: boolean;
allowedLabel?: string; allowedLabel?: string;
renderGuardBanner?: boolean; renderGuardBanner?: boolean;
tooltipProps?: Omit<TooltipProps, "children">;
} & BoundCanProps<TOrgPermission>; } & BoundCanProps<TOrgPermission>;
export const OrgPermissionCan: FunctionComponent<Props> = ({ export const OrgPermissionCan: FunctionComponent<Props> = ({
@@ -29,6 +31,7 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
renderTooltip, renderTooltip,
allowedLabel, allowedLabel,
renderGuardBanner, renderGuardBanner,
tooltipProps,
...props ...props
}) => { }) => {
const { permission } = useOrgPermission(); const { permission } = useOrgPermission();
@@ -43,11 +46,19 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
: children; : children;
if (!isAllowed && passThrough) { if (!isAllowed && passThrough) {
return <Tooltip content={label}>{finalChild}</Tooltip>; return (
<Tooltip content={label} {...tooltipProps}>
{finalChild}
</Tooltip>
);
} }
if (isAllowed && renderTooltip && allowedLabel) { if (isAllowed && renderTooltip && allowedLabel) {
return <Tooltip content={allowedLabel}>{finalChild}</Tooltip>; return (
<Tooltip content={allowedLabel} {...tooltipProps}>
{finalChild}
</Tooltip>
);
} }
if (!isAllowed && renderGuardBanner) { if (!isAllowed && renderGuardBanner) {
@@ -24,11 +24,17 @@ enum EnforceAuthType {
export const OrgGeneralAuthSection = ({ export const OrgGeneralAuthSection = ({
isSamlConfigured, isSamlConfigured,
isOidcConfigured, isOidcConfigured,
isGoogleConfigured isGoogleConfigured,
isSamlActive,
isOidcActive,
isLdapActive
}: { }: {
isSamlConfigured: boolean; isSamlConfigured: boolean;
isOidcConfigured: boolean; isOidcConfigured: boolean;
isGoogleConfigured: boolean; isGoogleConfigured: boolean;
isSamlActive: boolean;
isOidcActive: boolean;
isLdapActive: boolean;
}) => { }) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription } = useSubscription(); 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 ( return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<div> <div>
@@ -135,7 +150,7 @@ export const OrgGeneralAuthSection = ({
</p> </p>
</div> </div>
<div className="flex flex-col gap-2 py-4"> <div className="flex flex-col gap-2 py-4">
<div className={twMerge("mt-4", !isSamlConfigured && "hidden")}> <div className={twMerge("mt-4", (!isSamlConfigured || isGoogleOAuthEnforced) && "hidden")}>
<div className="mb-2 flex justify-between"> <div className="mb-2 flex justify-between">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce SAML SSO</span> <span className="text-md text-mineshaft-100">Enforce SAML SSO</span>
@@ -160,7 +175,7 @@ export const OrgGeneralAuthSection = ({
</p> </p>
</div> </div>
<div className={twMerge("mt-4", !isOidcConfigured && "hidden")}> <div className={twMerge("mt-4", (!isOidcConfigured || isGoogleOAuthEnforced) && "hidden")}>
<div className="mb-2 flex justify-between"> <div className="mb-2 flex justify-between">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce OIDC SSO</span> <span className="text-md text-mineshaft-100">Enforce OIDC SSO</span>
@@ -188,26 +203,47 @@ export const OrgGeneralAuthSection = ({
<div className={twMerge("mt-2", !isGoogleConfigured && "hidden")}> <div className={twMerge("mt-2", !isGoogleConfigured && "hidden")}>
<div className="mb-2 flex justify-between"> <div className="mb-2 flex justify-between">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-md text-mineshaft-100">Enforce Google SSO</span> <span className="text-md text-mineshaft-100">Enforce Google OAuth</span>
</div> </div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}> <OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Sso}
tooltipProps={{
className: "max-w-sm",
side: "left"
}}
allowedLabel={
isOidcActive || isSamlActive || isLdapActive
? `You cannot enforce Google OAuth while ${getActiveSsoLabel()} SSO is enabled. Disable ${getActiveSsoLabel()} SSO to enforce Google OAuth.`
: undefined
}
renderTooltip={isOidcActive || isSamlActive || isLdapActive}
>
{(isAllowed) => ( {(isAllowed) => (
<Switch <div>
id="enforce-google-sso" <Switch
onCheckedChange={(value) => id="enforce-google-sso"
handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE) onCheckedChange={(value) =>
} handleEnforceOrgAuthToggle(value, EnforceAuthType.GOOGLE)
isChecked={currentOrg?.googleSsoAuthEnforced ?? false} }
isDisabled={!isAllowed || currentOrg?.authEnforced} isChecked={currentOrg?.googleSsoAuthEnforced ?? false}
/> isDisabled={
!isAllowed ||
currentOrg?.authEnforced ||
isOidcActive ||
isSamlActive ||
isLdapActive
}
/>
</div>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
<p className="text-sm text-mineshaft-300"> <p className="text-sm text-mineshaft-300">
Enforce users to authenticate via Google OAuth SSO to access this organization. Enforce users to authenticate via Google OAuth to access this organization.
<br /> <br />
When this is enabled your organization members will only be able to login with Google When this is enabled your organization members will only be able to login with Google
SSO (not Google SAML). OAuth (not Google SAML).
</p> </p>
</div> </div>
</div> </div>
@@ -267,8 +303,8 @@ export const OrgGeneralAuthSection = ({
</div> </div>
<p className="text-sm text-mineshaft-300"> <p className="text-sm text-mineshaft-300">
<span> <span>
Allow organization admins to bypass SAML enforcement when SSO is unavailable, Allow organization admins to bypass SSO login enforcement when your SSO provider is
misconfigured, or inaccessible. unavailable, misconfigured, or inaccessible.
</span> </span>
</p> </p>
</div> </div>
@@ -94,6 +94,8 @@ export const OrgLDAPSection = (): JSX.Element => {
handlePopUpOpen("ldapGroupMap"); handlePopUpOpen("ldapGroupMap");
}; };
const isGoogleOAuthEnabled = currentOrg.googleSsoAuthEnforced;
return ( return (
<div className="mb-4"> <div className="mb-4">
<div className="py-4"> <div className="py-4">
@@ -116,16 +118,31 @@ export const OrgLDAPSection = (): JSX.Element => {
<div className="pt-4"> <div className="pt-4">
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">Enable LDAP</h2> <h2 className="text-md text-mineshaft-100">Enable LDAP</h2>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}> <OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Ldap}
tooltipProps={{
className: "max-w-sm",
side: "left"
}}
allowedLabel={
isGoogleOAuthEnabled
? "You cannot enable LDAP SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable LDAP SSO."
: undefined
}
renderTooltip={isGoogleOAuthEnabled}
>
{(isAllowed) => ( {(isAllowed) => (
<Switch <div>
id="enable-saml-sso" <Switch
onCheckedChange={(value) => handleLDAPToggle(value)} id="enable-saml-sso"
isChecked={data ? data.isActive : false} onCheckedChange={(value) => handleLDAPToggle(value)}
isDisabled={!isAllowed} isChecked={data ? data.isActive : false}
> isDisabled={!isAllowed || isGoogleOAuthEnabled}
Enable >
</Switch> Enable
</Switch>
</div>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
@@ -83,6 +83,8 @@ export const OrgOIDCSection = (): JSX.Element => {
} }
}; };
const isGoogleOAuthEnabled = currentOrg.googleSsoAuthEnforced;
return ( return (
<div className="mb-4 rounded-lg border-mineshaft-600 bg-mineshaft-900"> <div className="mb-4 rounded-lg border-mineshaft-600 bg-mineshaft-900">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
@@ -106,14 +108,29 @@ export const OrgOIDCSection = (): JSX.Element => {
<div className="mb-2 flex items-center justify-between"> <div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">Enable OIDC</h2> <h2 className="text-md text-mineshaft-100">Enable OIDC</h2>
{!isPending && ( {!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}> <OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Sso}
tooltipProps={{
className: "max-w-sm",
side: "left"
}}
allowedLabel={
isGoogleOAuthEnabled
? "You cannot enable OIDC SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable OIDC SSO."
: undefined
}
renderTooltip={isGoogleOAuthEnabled}
>
{(isAllowed) => ( {(isAllowed) => (
<Switch <div>
id="enable-oidc-sso" <Switch
onCheckedChange={(value) => handleOIDCToggle(value)} id="enable-oidc-sso"
isChecked={data ? data.isActive : false} onCheckedChange={(value) => handleOIDCToggle(value)}
isDisabled={!isAllowed} isChecked={data ? data.isActive : false}
/> isDisabled={!isAllowed || isGoogleOAuthEnabled}
/>
</div>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
)} )}
@@ -78,6 +78,8 @@ export const OrgSSOSection = (): JSX.Element => {
} }
}; };
const isGoogleOAuthEnabled = currentOrg.googleSsoAuthEnforced;
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
@@ -99,14 +101,29 @@ export const OrgSSOSection = (): JSX.Element => {
<div className="mb-2 flex items-center justify-between pt-4"> <div className="mb-2 flex items-center justify-between pt-4">
<h2 className="text-md text-mineshaft-100">Enable SAML</h2> <h2 className="text-md text-mineshaft-100">Enable SAML</h2>
{!isPending && ( {!isPending && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}> <OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Sso}
tooltipProps={{
className: "max-w-sm",
side: "left"
}}
allowedLabel={
isGoogleOAuthEnabled
? "You cannot enable SAML SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable SAML SSO."
: undefined
}
renderTooltip={isGoogleOAuthEnabled}
>
{(isAllowed) => ( {(isAllowed) => (
<Switch <div>
id="enable-saml-sso" <Switch
onCheckedChange={(value) => handleSamlSSOToggle(value)} id="enable-saml-sso"
isChecked={data ? data.isActive : false} onCheckedChange={(value) => handleSamlSSOToggle(value)}
isDisabled={!isAllowed} isChecked={data ? data.isActive : false}
/> isDisabled={!isAllowed || isGoogleOAuthEnabled}
/>
</div>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
)} )}
@@ -184,6 +184,9 @@ export const OrgSsoTab = withPermission(
isSamlConfigured={isSamlConfigured} isSamlConfigured={isSamlConfigured}
isOidcConfigured={isOidcConfigured} isOidcConfigured={isOidcConfigured}
isGoogleConfigured={isGoogleConfigured} isGoogleConfigured={isGoogleConfigured}
isSamlActive={Boolean(samlConfig?.isActive)}
isOidcActive={Boolean(oidcConfig?.isActive)}
isLdapActive={Boolean(ldapConfig?.isActive)}
/> />
)} )}