From 2810632f4afa25d02e5057d123b8a0694436b176 Mon Sep 17 00:00:00 2001
From: miloschwartz
Date: Wed, 7 Jan 2026 20:40:59 -0800
Subject: [PATCH] add flag to enable org only idp in ee
---
server/private/lib/config.ts | 4 ++
server/private/lib/readConfigFile.ts | 3 +-
.../loginPage/upsertLoginPageBranding.ts | 7 +++-
.../[orgId]/settings/(private)/idp/layout.tsx | 18 ++++++++
src/app/[orgId]/settings/layout.tsx | 2 +-
.../proxy/[niceId]/authentication/page.tsx | 9 ++--
src/app/admin/layout.tsx | 5 ++-
src/app/auth/login/page.tsx | 4 +-
src/app/auth/org/[orgId]/page.tsx | 41 +++++++++----------
src/app/auth/org/page.tsx | 6 +--
src/app/auth/resource/[resourceGuid]/page.tsx | 2 +-
src/app/navigation.tsx | 27 ++++++------
src/components/AuthPageBrandingForm.tsx | 6 ++-
src/lib/pullEnv.ts | 4 +-
src/lib/queries.ts | 15 ++++++-
src/lib/types/env.ts | 1 +
16 files changed, 101 insertions(+), 53 deletions(-)
create mode 100644 src/app/[orgId]/settings/(private)/idp/layout.tsx
diff --git a/server/private/lib/config.ts b/server/private/lib/config.ts
index 97baf1e0..ae9ca5c7 100644
--- a/server/private/lib/config.ts
+++ b/server/private/lib/config.ts
@@ -139,6 +139,10 @@ export class PrivateConfig {
process.env.USE_PANGOLIN_DNS =
this.rawPrivateConfig.flags.use_pangolin_dns.toString();
}
+ if (this.rawPrivateConfig.flags.use_org_only_idp) {
+ process.env.USE_ORG_ONLY_IDP =
+ this.rawPrivateConfig.flags.use_org_only_idp.toString();
+ }
}
public getRawPrivateConfig() {
diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts
index c986e62d..374dee7c 100644
--- a/server/private/lib/readConfigFile.ts
+++ b/server/private/lib/readConfigFile.ts
@@ -83,7 +83,8 @@ export const privateConfigSchema = z.object({
flags: z
.object({
enable_redis: z.boolean().optional().default(false),
- use_pangolin_dns: z.boolean().optional().default(false)
+ use_pangolin_dns: z.boolean().optional().default(false),
+ use_org_only_idp: z.boolean().optional().default(false)
})
.optional()
.prefault({}),
diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts
index f9f9d08c..4e2b666b 100644
--- a/server/private/routers/loginPage/upsertLoginPageBranding.ts
+++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts
@@ -28,6 +28,7 @@ import { eq, InferInsertModel } from "drizzle-orm";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
+import config from "@server/private/lib/config";
const paramsSchema = z.strictObject({
orgId: z.string()
@@ -94,8 +95,10 @@ export async function upsertLoginPageBranding(
typeof loginPageBranding
>;
- if (build !== "saas") {
- // org branding settings are only considered in the saas build
+ if (
+ build !== "saas" &&
+ !config.getRawPrivateConfig().flags.use_org_only_idp
+ ) {
const { orgTitle, orgSubtitle, ...rest } = updateData;
updateData = rest;
}
diff --git a/src/app/[orgId]/settings/(private)/idp/layout.tsx b/src/app/[orgId]/settings/(private)/idp/layout.tsx
new file mode 100644
index 00000000..dcb73afb
--- /dev/null
+++ b/src/app/[orgId]/settings/(private)/idp/layout.tsx
@@ -0,0 +1,18 @@
+import { pullEnv } from "@app/lib/pullEnv";
+import { build } from "@server/build";
+import { redirect } from "next/navigation";
+
+interface LayoutProps {
+ children: React.ReactNode;
+ params: Promise<{}>;
+}
+
+export default async function Layout(props: LayoutProps) {
+ const env = pullEnv();
+
+ if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
+ redirect("/");
+ }
+
+ return props.children;
+}
diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx
index 8f44c23c..34ed3ac2 100644
--- a/src/app/[orgId]/settings/layout.tsx
+++ b/src/app/[orgId]/settings/layout.tsx
@@ -82,7 +82,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
{children}
diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx
index 968b2700..f021076f 100644
--- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx
+++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx
@@ -36,8 +36,8 @@ import {
import type { ResourceContextType } from "@app/contexts/resourceContext";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
+import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { useResourceContext } from "@app/hooks/useResourceContext";
-import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { orgQueries, resourceQueries } from "@app/lib/queries";
@@ -95,7 +95,7 @@ export default function ResourceAuthenticationPage() {
const router = useRouter();
const t = useTranslations();
- const subscription = useSubscriptionStatusContext();
+ const { isPaidUser } = usePaidStatus();
const queryClient = useQueryClient();
const { data: resourceRoles = [], isLoading: isLoadingResourceRoles } =
@@ -129,7 +129,8 @@ export default function ResourceAuthenticationPage() {
);
const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery(
orgQueries.identityProviders({
- orgId: org.org.orgId
+ orgId: org.org.orgId,
+ useOrgOnlyIdp: env.flags.useOrgOnlyIdp
})
);
@@ -159,7 +160,7 @@ export default function ResourceAuthenticationPage() {
const allIdps = useMemo(() => {
if (build === "saas") {
- if (subscription?.subscribed) {
+ if (isPaidUser) {
return orgIdps.map((idp) => ({
id: idp.idpId,
text: idp.name
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
index 060f18ac..44d85b99 100644
--- a/src/app/admin/layout.tsx
+++ b/src/app/admin/layout.tsx
@@ -11,6 +11,7 @@ import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies";
import { Layout } from "@app/components/Layout";
import { adminNavSections } from "../navigation";
+import { pullEnv } from "@app/lib/pullEnv";
export const dynamic = "force-dynamic";
@@ -27,6 +28,8 @@ export default async function AdminLayout(props: LayoutProps) {
const getUser = cache(verifySession);
const user = await getUser();
+ const env = pullEnv();
+
if (!user || !user.serverAdmin) {
redirect(`/`);
}
@@ -48,7 +51,7 @@ export default async function AdminLayout(props: LayoutProps) {
return (
-
+
{props.children}
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx
index bd6327fd..fb327af5 100644
--- a/src/app/auth/login/page.tsx
+++ b/src/app/auth/login/page.tsx
@@ -70,7 +70,7 @@ export default async function Page(props: {
}
let loginIdps: LoginFormIDP[] = [];
- if (build !== "saas") {
+ if (build === "oss" || !env.flags.useOrgOnlyIdp) {
const idpsRes = await cache(
async () => await priv.get>("/idp")
)();
@@ -121,7 +121,7 @@ export default async function Page(props: {
)}
- {!isInvite && build === "saas" ? (
+ {!isInvite && (build === "saas" || env.flags.useOrgOnlyIdp) ? (
{t("needToSignInToOrg")}
>(
- `/org/${orgId}/idp`
- );
+ const idpsRes = await priv.get
>(
+ `/org/${orgId}/idp`
+ );
- loginIdps = idpsRes.data.data.idps.map((idp) => ({
- idpId: idp.idpId,
- name: idp.name,
- variant: idp.variant
- })) as LoginFormIDP[];
- }
+ loginIdps = idpsRes.data.data.idps.map((idp) => ({
+ idpId: idp.idpId,
+ name: idp.name,
+ variant: idp.variant
+ })) as LoginFormIDP[];
let branding: LoadLoginPageBrandingResponse | null = null;
- if (build === "saas") {
- try {
- const res = await priv.get<
- AxiosResponse
- >(`/login-page-branding?orgId=${orgId}`);
- if (res.status === 200) {
- branding = res.data.data;
- }
- } catch (error) {}
- }
+ try {
+ const res = await priv.get<
+ AxiosResponse
+ >(`/login-page-branding?orgId=${orgId}`);
+ if (res.status === 200) {
+ branding = res.data.data;
+ }
+ } catch (error) {}
return (
diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx
index 54576c0c..345b5a4c 100644
--- a/src/app/navigation.tsx
+++ b/src/app/navigation.tsx
@@ -1,4 +1,5 @@
import { SidebarNavItem } from "@app/components/SidebarNav";
+import { Env } from "@app/lib/types/env";
import { build } from "@server/build";
import {
Settings,
@@ -39,7 +40,7 @@ export const orgLangingNavItems: SidebarNavItem[] = [
}
];
-export const orgNavSections = (): SidebarNavSection[] => [
+export const orgNavSections = (env?: Env): SidebarNavSection[] => [
{
heading: "sidebarGeneral",
items: [
@@ -92,8 +93,7 @@ export const orgNavSections = (): SidebarNavSection[] => [
{
title: "sidebarRemoteExitNodes",
href: "/{orgId}/settings/remote-exit-nodes",
- icon: ,
- showEE: true
+ icon:
}
]
: [])
@@ -123,13 +123,12 @@ export const orgNavSections = (): SidebarNavSection[] => [
href: "/{orgId}/settings/access/roles",
icon:
},
- ...(build == "saas"
+ ...(build == "saas" || env?.flags.useOrgOnlyIdp
? [
{
title: "sidebarIdentityProviders",
href: "/{orgId}/settings/idp",
- icon: ,
- showEE: true
+ icon:
}
]
: []),
@@ -228,7 +227,7 @@ export const orgNavSections = (): SidebarNavSection[] => [
}
];
-export const adminNavSections: SidebarNavSection[] = [
+export const adminNavSections = (env?: Env): SidebarNavSection[] => [
{
heading: "sidebarAdmin",
items: [
@@ -242,11 +241,15 @@ export const adminNavSections: SidebarNavSection[] = [
href: "/admin/api-keys",
icon:
},
- {
- title: "sidebarIdentityProviders",
- href: "/admin/idp",
- icon:
- },
+ ...(build === "oss" || !env?.flags.useOrgOnlyIdp
+ ? [
+ {
+ title: "sidebarIdentityProviders",
+ href: "/admin/idp",
+ icon:
+ }
+ ]
+ : []),
...(build == "enterprise"
? [
{
diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx
index 67e2232f..119a39cb 100644
--- a/src/components/AuthPageBrandingForm.tsx
+++ b/src/components/AuthPageBrandingForm.tsx
@@ -118,6 +118,7 @@ export default function AuthPageBrandingForm({
const brandingData = form.getValues();
if (!isValid || !isPaidUser) return;
+
try {
const updateRes = await api.put(
`/org/${orgId}/login-page-branding`,
@@ -289,7 +290,8 @@ export default function AuthPageBrandingForm({
- {build === "saas" && (
+ {build === "saas" ||
+ env.env.flags.useOrgOnlyIdp ? (
<>
@@ -343,7 +345,7 @@ export default function AuthPageBrandingForm({
/>
>
- )}
+ ) : null}
diff --git a/src/lib/pullEnv.ts b/src/lib/pullEnv.ts
index ed0057de..319e08f0 100644
--- a/src/lib/pullEnv.ts
+++ b/src/lib/pullEnv.ts
@@ -63,7 +63,9 @@ export function pullEnv(): Env {
disableProductHelpBanners:
process.env.FLAGS_DISABLE_PRODUCT_HELP_BANNERS === "true"
? true
- : false
+ : false,
+ useOrgOnlyIdp:
+ process.env.USE_ORG_ONLY_IDP === "true" ? true : false
},
branding: {
diff --git a/src/lib/queries.ts b/src/lib/queries.ts
index 5ea3c2f2..2034bba8 100644
--- a/src/lib/queries.ts
+++ b/src/lib/queries.ts
@@ -157,7 +157,13 @@ export const orgQueries = {
return res.data.data.domains;
}
}),
- identityProviders: ({ orgId }: { orgId: string }) =>
+ identityProviders: ({
+ orgId,
+ useOrgOnlyIdp
+ }: {
+ orgId: string;
+ useOrgOnlyIdp?: boolean;
+ }) =>
queryOptions({
queryKey: ["ORG", orgId, "IDPS"] as const,
queryFn: async ({ signal, meta }) => {
@@ -165,7 +171,12 @@ export const orgQueries = {
AxiosResponse<{
idps: { idpId: number; name: string }[];
}>
- >(build === "saas" ? `/org/${orgId}/idp` : "/idp", { signal });
+ >(
+ build === "saas" || useOrgOnlyIdp
+ ? `/org/${orgId}/idp`
+ : "/idp",
+ { signal }
+ );
return res.data.data.idps;
}
})
diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts
index 1f54f680..f99e1994 100644
--- a/src/lib/types/env.ts
+++ b/src/lib/types/env.ts
@@ -34,6 +34,7 @@ export type Env = {
hideSupporterKey: boolean;
usePangolinDns: boolean;
disableProductHelpBanners: boolean;
+ useOrgOnlyIdp: boolean;
};
branding: {
appName?: string;