mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-06 02:36:38 +00:00
add identity provider mode setting
This commit is contained in:
@@ -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, {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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}` : ""}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 () =>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user