add identity provider mode setting

This commit is contained in:
miloschwartz
2026-02-11 18:04:56 -08:00
parent 937f6fdae8
commit 143acbae48
17 changed files with 481 additions and 395 deletions

View File

@@ -79,6 +79,7 @@ export const subscriptionItems = sqliteTable("subscriptionItems", {
subscriptionItemId: integer("subscriptionItemId").primaryKey({ subscriptionItemId: integer("subscriptionItemId").primaryKey({
autoIncrement: true autoIncrement: true
}), }),
stripeSubscriptionItemId: text("stripeSubscriptionItemId"),
subscriptionId: text("subscriptionId") subscriptionId: text("subscriptionId")
.notNull() .notNull()
.references(() => subscriptions.subscriptionId, { .references(() => subscriptions.subscriptionId, {

View File

@@ -65,6 +65,11 @@ export class PrivateConfig {
this.rawPrivateConfig.branding?.logo?.dark_path || undefined; this.rawPrivateConfig.branding?.logo?.dark_path || undefined;
} }
if (this.rawPrivateConfig.app.identity_provider_mode) {
process.env.IDENTITY_PROVIDER_MODE =
this.rawPrivateConfig.app.identity_provider_mode;
}
process.env.BRANDING_LOGO_AUTH_WIDTH = this.rawPrivateConfig.branding process.env.BRANDING_LOGO_AUTH_WIDTH = this.rawPrivateConfig.branding
?.logo?.auth_page?.width ?.logo?.auth_page?.width
? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString() ? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString()
@@ -129,10 +134,8 @@ export class PrivateConfig {
process.env.USE_PANGOLIN_DNS = process.env.USE_PANGOLIN_DNS =
this.rawPrivateConfig.flags.use_pangolin_dns.toString(); this.rawPrivateConfig.flags.use_pangolin_dns.toString();
} }
if (this.rawPrivateConfig.flags.use_org_only_idp) {
process.env.USE_ORG_ONLY_IDP = console.log(this.rawPrivateConfig.app.identity_provider_mode);
this.rawPrivateConfig.flags.use_org_only_idp.toString();
}
} }
public getRawPrivateConfig() { public getRawPrivateConfig() {

View File

@@ -25,7 +25,8 @@ export const privateConfigSchema = z.object({
app: z app: z
.object({ .object({
region: z.string().optional().default("default"), region: z.string().optional().default("default"),
base_domain: z.string().optional() base_domain: z.string().optional(),
identity_provider_mode: z.enum(["global", "org"]).optional()
}) })
.optional() .optional()
.default({ .default({
@@ -95,7 +96,7 @@ export const privateConfigSchema = z.object({
.object({ .object({
enable_redis: z.boolean().optional().default(false), 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), use_org_only_idp: z.boolean().optional()
}) })
.optional() .optional()
.prefault({}), .prefault({}),
@@ -181,6 +182,28 @@ export const privateConfigSchema = z.object({
// localFilePath: z.string().optional() // localFilePath: z.string().optional()
}) })
.optional() .optional()
})
.transform((data) => {
// this to maintain backwards compatibility with the old config file
const identityProviderMode = data.app?.identity_provider_mode;
const useOrgOnlyIdp = data.flags?.use_org_only_idp;
if (identityProviderMode !== undefined) {
return data;
}
if (useOrgOnlyIdp === true) {
return {
...data,
app: { ...data.app, identity_provider_mode: "org" as const }
};
}
if (useOrgOnlyIdp === false) {
return {
...data,
app: { ...data.app, identity_provider_mode: "global" as const }
};
}
return data;
}); });
export function readPrivateConfigFile() { export function readPrivateConfigFile() {

View File

@@ -27,6 +27,7 @@ import config from "@server/lib/config";
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types"; import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
import { isSubscribed } from "#private/lib/isSubscribed"; import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import privateConfig from "#private/lib/config";
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
@@ -92,6 +93,18 @@ export async function createOrgOidcIdp(
); );
} }
if (
privateConfig.getRawPrivateConfig().app.identity_provider_mode !==
"org"
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature."
)
);
}
const { const {
clientId, clientId,
clientSecret, clientSecret,

View File

@@ -22,6 +22,7 @@ import { fromError } from "zod-validation-error";
import { idp, idpOidcConfig, idpOrg } from "@server/db"; import { idp, idpOidcConfig, idpOrg } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import privateConfig from "#private/lib/config";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -59,6 +60,18 @@ export async function deleteOrgIdp(
const { idpId } = parsedParams.data; const { idpId } = parsedParams.data;
if (
privateConfig.getRawPrivateConfig().app.identity_provider_mode !==
"org"
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature."
)
);
}
// Check if IDP exists // Check if IDP exists
const [existingIdp] = await db const [existingIdp] = await db
.select() .select()

View File

@@ -26,6 +26,7 @@ import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { isSubscribed } from "#private/lib/isSubscribed"; import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import privateConfig from "#private/lib/config";
const paramsSchema = z const paramsSchema = z
.object({ .object({
@@ -97,6 +98,18 @@ export async function updateOrgOidcIdp(
); );
} }
if (
privateConfig.getRawPrivateConfig().app.identity_provider_mode !==
"org"
) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature."
)
);
}
const { idpId, orgId } = parsedParams.data; const { idpId, orgId } = parsedParams.data;
const { const {
clientId, clientId,

View File

@@ -132,7 +132,7 @@ export default function ResourceAuthenticationPage() {
const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery(
orgQueries.identityProviders({ orgQueries.identityProviders({
orgId: org.org.orgId, orgId: org.org.orgId,
useOrgOnlyIdp: env.flags.useOrgOnlyIdp useOrgOnlyIdp: env.app.identityProviderMode === "org"
}) })
); );

View File

@@ -76,12 +76,13 @@ export default async function Page(props: {
// Only use SmartLoginForm if NOT (OSS build OR org-only IdP enabled) // Only use SmartLoginForm if NOT (OSS build OR org-only IdP enabled)
const useSmartLogin = const useSmartLogin =
build === "saas" || (build === "enterprise" && env.flags.useOrgOnlyIdp); build === "saas" ||
(build === "enterprise" && env.app.identityProviderMode === "org");
let loginIdps: LoginFormIDP[] = []; let loginIdps: LoginFormIDP[] = [];
if (!useSmartLogin) { if (!useSmartLogin) {
// Load IdPs for DashboardLoginForm (OSS or org-only IdP mode) // Load IdPs for DashboardLoginForm (OSS or org-only IdP mode)
if (build === "oss" || !env.flags.useOrgOnlyIdp) { if (build === "oss" || env.app.identityProviderMode !== "org") {
const idpsRes = await cache( const idpsRes = await cache(
async () => async () =>
await priv.get<AxiosResponse<ListIdpsResponse>>("/idp") await priv.get<AxiosResponse<ListIdpsResponse>>("/idp")
@@ -165,7 +166,8 @@ export default async function Page(props: {
forceLogin={forceLogin} forceLogin={forceLogin}
showOrgLogin={ showOrgLogin={
!isInvite && !isInvite &&
(build === "saas" || env.flags.useOrgOnlyIdp) (build === "saas" ||
env.app.identityProviderMode === "org")
} }
searchParams={searchParams} searchParams={searchParams}
defaultUser={defaultUser} defaultUser={defaultUser}
@@ -188,7 +190,8 @@ export default async function Page(props: {
</p> </p>
)} )}
{!isInvite && (build === "saas" || env.flags.useOrgOnlyIdp) ? ( {!isInvite &&
(build === "saas" || env.app.identityProviderMode === "org") ? (
<OrgSignInLink <OrgSignInLink
href={`/auth/org${buildQueryString(searchParams)}`} href={`/auth/org${buildQueryString(searchParams)}`}
linkText={t("orgAuthSignInToOrg")} linkText={t("orgAuthSignInToOrg")}

View File

@@ -24,7 +24,7 @@ export default async function OrgAuthPage(props: {
const env = pullEnv(); const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) { if (build !== "saas" && env.app.identityProviderMode !== "org") {
const queryString = new URLSearchParams(searchParams as any).toString(); const queryString = new URLSearchParams(searchParams as any).toString();
redirect(`/auth/login${queryString ? `?${queryString}` : ""}`); redirect(`/auth/login${queryString ? `?${queryString}` : ""}`);
} }

View File

@@ -35,7 +35,7 @@ export default async function OrgAuthPage(props: {
const env = pullEnv(); const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) { if (build !== "saas" && env.app.identityProviderMode !== "org") {
redirect("/"); redirect("/");
} }

View File

@@ -202,7 +202,7 @@ export default async function ResourceAuthPage(props: {
} }
let loginIdps: LoginFormIDP[] = []; let loginIdps: LoginFormIDP[] = [];
if (build === "saas" || env.flags.useOrgOnlyIdp) { if (build === "saas" || env.app.identityProviderMode === "org") {
if (subscribed) { if (subscribed) {
const idpsRes = await cache( const idpsRes = await cache(
async () => async () =>

View File

@@ -124,7 +124,8 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
// PaidFeaturesAlert // PaidFeaturesAlert
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) || ...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
build === "saas" || build === "saas" ||
env?.flags.useOrgOnlyIdp env?.app.identityProviderMode === "org" ||
env?.app.identityProviderMode === undefined
? [ ? [
{ {
title: "sidebarIdentityProviders", title: "sidebarIdentityProviders",
@@ -251,7 +252,9 @@ export const adminNavSections = (env?: Env): SidebarNavSection[] => [
href: "/admin/api-keys", href: "/admin/api-keys",
icon: <KeyRound className="size-4 flex-none" /> icon: <KeyRound className="size-4 flex-none" />
}, },
...(build === "oss" || !env?.flags.useOrgOnlyIdp ...(build === "oss" ||
env?.app.identityProviderMode === "global" ||
env?.app.identityProviderMode === undefined
? [ ? [
{ {
title: "sidebarIdentityProviders", title: "sidebarIdentityProviders",

View File

@@ -322,7 +322,7 @@ export default function AuthPageBrandingForm({
</div> </div>
{build === "saas" || {build === "saas" ||
env.env.flags.useOrgOnlyIdp ? ( env.env.app.identityProviderMode === "org" ? (
<> <>
<div className="mt-3 mb-6"> <div className="mt-3 mb-6">
<SettingsSectionTitle> <SettingsSectionTitle>

View File

@@ -118,7 +118,7 @@ function getActionsCategories(root: boolean) {
} }
}; };
if (root || build === "saas" || env.flags.useOrgOnlyIdp) { if (root || build === "saas" || env.app.identityProviderMode === "org") {
actionsByCategory["Identity Provider (IDP)"] = { actionsByCategory["Identity Provider (IDP)"] = {
[t("actionCreateIdp")]: "createIdp", [t("actionCreateIdp")]: "createIdp",
[t("actionUpdateIdp")]: "updateIdp", [t("actionUpdateIdp")]: "updateIdp",

View File

@@ -204,7 +204,9 @@ export default function SignupForm({
? env.branding.logo?.authPage?.height || 44 ? env.branding.logo?.authPage?.height || 44
: 44; : 44;
const showOrgBanner = fromSmartLogin && (build === "saas" || env.flags.useOrgOnlyIdp); const showOrgBanner =
fromSmartLogin &&
(build === "saas" || env.app.identityProviderMode === "org");
const orgBannerHref = redirect const orgBannerHref = redirect
? `/auth/org?redirect=${encodeURIComponent(redirect)}` ? `/auth/org?redirect=${encodeURIComponent(redirect)}`
: "/auth/org"; : "/auth/org";
@@ -262,7 +264,9 @@ export default function SignupForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FormLabel>{t("password")}</FormLabel> <FormLabel>
{t("password")}
</FormLabel>
{passwordStrength.strength === {passwordStrength.strength ===
"strong" && ( "strong" && (
<Check className="h-4 w-4 text-green-500" /> <Check className="h-4 w-4 text-green-500" />
@@ -303,7 +307,9 @@ export default function SignupForm({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium text-foreground"> <span className="text-sm font-medium text-foreground">
{t("passwordStrength")} {t(
"passwordStrength"
)}
</span> </span>
<span <span
className={cn( className={cn(
@@ -335,7 +341,9 @@ export default function SignupForm({
{/* Requirements Checklist */} {/* Requirements Checklist */}
<div className="bg-muted rounded-lg p-3 space-y-2"> <div className="bg-muted rounded-lg p-3 space-y-2">
<div className="text-sm font-medium text-foreground mb-2"> <div className="text-sm font-medium text-foreground mb-2">
{t("passwordRequirements")} {t(
"passwordRequirements"
)}
</div> </div>
<div className="grid grid-cols-1 gap-1.5"> <div className="grid grid-cols-1 gap-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -527,7 +535,9 @@ export default function SignupForm({
onCheckedChange={( onCheckedChange={(
checked checked
) => { ) => {
field.onChange(checked); field.onChange(
checked
);
handleTermsChange( handleTermsChange(
checked as boolean checked as boolean
); );
@@ -550,7 +560,9 @@ export default function SignupForm({
"signUpTerms.termsOfService" "signUpTerms.termsOfService"
)}{" "} )}{" "}
</a> </a>
{t("signUpTerms.and")}{" "} {t(
"signUpTerms.and"
)}{" "}
<a <a
href="https://pangolin.net/privacy-policy.html" href="https://pangolin.net/privacy-policy.html"
target="_blank" target="_blank"

View File

@@ -32,7 +32,11 @@ export function pullEnv(): Env {
process.env.NEW_RELEASES_NOTIFICATION_ENABLED === "true" process.env.NEW_RELEASES_NOTIFICATION_ENABLED === "true"
? true ? true
: false : false
} },
identityProviderMode: process.env.IDENTITY_PROVIDER_MODE as
| "org"
| "global"
| undefined
}, },
email: { email: {
emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
@@ -64,8 +68,6 @@ export function pullEnv(): Env {
process.env.FLAGS_DISABLE_PRODUCT_HELP_BANNERS === "true" process.env.FLAGS_DISABLE_PRODUCT_HELP_BANNERS === "true"
? true ? true
: false, : false,
useOrgOnlyIdp:
process.env.USE_ORG_ONLY_IDP === "true" ? true : false,
disableEnterpriseFeatures: disableEnterpriseFeatures:
process.env.DISABLE_ENTERPRISE_FEATURES === "true" process.env.DISABLE_ENTERPRISE_FEATURES === "true"
? true ? true

View File

@@ -8,6 +8,7 @@ export type Env = {
product_updates: boolean; product_updates: boolean;
new_releases: boolean; new_releases: boolean;
}; };
identityProviderMode?: "global" | "org";
}; };
server: { server: {
externalPort: string; externalPort: string;
@@ -34,7 +35,6 @@ export type Env = {
hideSupporterKey: boolean; hideSupporterKey: boolean;
usePangolinDns: boolean; usePangolinDns: boolean;
disableProductHelpBanners: boolean; disableProductHelpBanners: boolean;
useOrgOnlyIdp: boolean;
disableEnterpriseFeatures: boolean; disableEnterpriseFeatures: boolean;
}; };
branding: { branding: {