Compare commits

..

2 Commits

Author SHA1 Message Date
miloschwartz
0a537c6830 add org only idp to integration api 2026-01-11 10:47:19 -08:00
miloschwartz
2810632f4a add flag to enable org only idp in ee 2026-01-07 20:40:59 -08:00
35 changed files with 329 additions and 312 deletions

View File

@@ -1,125 +0,0 @@
name: CI/CD Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
permissions:
contents: read
packages: write # for GHCR push
id-token: write # for Cosign Keyless (OIDC) Signing
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+-s.[0-9]+"
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
pre-run:
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Start EC2 instances
run: |
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
echo "EC2 instances started"
release-arm:
name: Build and Release (ARM64)
runs-on: [self-hosted, linux, arm64, us-east-1]
needs: [pre-run]
if: >-
${{
needs.pre-run.result == 'success'
}}
# Job-level timeout to avoid runaway or stuck runs
timeout-minutes: 120
env:
# Target images
AWS_IMAGE: ${{ secrets.aws_account_id }}.dkr.ecr.us-east-1.amazonaws.com/${{ github.event.repository.name }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Monitor storage space
run: |
THRESHOLD=75
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
echo "Used space: $USED_SPACE%"
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
echo "Used space is below the threshold of 75% free. Running Docker system prune."
echo y | docker system prune -a
else
echo "Storage space is above the threshold. No action needed."
fi
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Extract tag name
id: get-tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
shell: bash
- name: Update version in package.json
run: |
TAG=${{ env.TAG }}
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
cat server/lib/consts.ts
shell: bash
- name: Build and push Docker images (Docker Hub - ARM64)
run: |
TAG=${{ env.TAG }}
make build-saas tag=$TAG
echo "Built & pushed ARM64 images to: ${{ env.AWS_IMAGE }}:${TAG}"
shell: bash
post-run:
needs: [pre-run, release-arm]
if: >-
${{
always() &&
needs.pre-run.result == 'success' &&
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure')
}}
runs-on: ubuntu-latest
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
aws-region: ${{ secrets.AWS_REGION }}
- name: Verify AWS identity
run: aws sts get-caller-identity
- name: Stop EC2 instances
run: |
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
echo "EC2 instances stopped"

View File

@@ -67,18 +67,6 @@ build-ee-postgresql:
--tag fosrl/pangolin:ee-postgresql-$(tag) \
--push .
build-saas:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \
fi
docker buildx build \
--build-arg BUILD=saas \
--build-arg DATABASE=pg \
--platform linux/arm64 \
--tag $(AWS_IMAGE):$(tag) \
--push .
build-release-arm:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \

View File

@@ -43,12 +43,9 @@ entryPoints:
http:
tls:
certResolver: "letsencrypt"
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true
serversTransport:
insecureSkipVerify: true
ping:
entryPoint: "web"
entryPoint: "web"

View File

@@ -340,7 +340,7 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration
fmt.Println("\n=== Basic Configuration ===")
config.IsEnterprise = readBoolNoDefault(reader, "Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
config.IsEnterprise = readBoolNoDefault(reader, "Do you want to install the Enterprise version of Pangolin? The EE is free for persoal use or for businesses making less than 100k USD annually.")
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")

View File

@@ -2244,7 +2244,7 @@
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
"deviceAuthorize": "Authorize {applicationName}",
"deviceConnected": "Device Connected!",
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
"deviceAuthorizedMessage": "Device is authorized to access your account.",
"pangolinCloud": "Pangolin Cloud",
"viewDevices": "View Devices",
"viewDevicesDescription": "Manage your connected devices",

View File

@@ -13,3 +13,4 @@ export * from "./verifyApiKeyIsRoot";
export * from "./verifyApiKeyApiKeyAccess";
export * from "./verifyApiKeyClientAccess";
export * from "./verifyApiKeySiteResourceAccess";
export * from "./verifyApiKeyIdpAccess";

View File

@@ -0,0 +1,88 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { idp, idpOrg, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeyIdpAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const idpId = req.params.idpId || req.body.idpId || req.query.idpId;
const orgId = req.params.orgId;
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (!idpId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid IDP ID")
);
}
if (apiKey.isRoot) {
// Root keys can access any IDP in any org
return next();
}
const [idpRes] = await db
.select()
.from(idp)
.innerJoin(idpOrg, eq(idp.idpId, idpOrg.idpId))
.where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId)))
.limit(1);
if (!idpRes || !idpRes.idp || !idpRes.idpOrg) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`IdP with ID ${idpId} not found for organization ${orgId}`
)
);
}
if (!req.apiKeyOrg) {
const apiKeyOrgRes = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, idpRes.idpOrg.orgId)
)
);
req.apiKeyOrg = apiKeyOrgRes[0];
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying IDP access"
)
);
}
}

View File

@@ -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() {

View File

@@ -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({}),

View File

@@ -456,11 +456,11 @@ export async function getTraefikConfig(
// );
} else if (resource.maintenanceModeType === "automatic") {
showMaintenancePage = !hasHealthyServers;
// if (showMaintenancePage) {
// logger.warn(
// `Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)`
// );
// }
if (showMaintenancePage) {
logger.warn(
`Resource ${resource.name} (${fullDomain}) has no healthy servers - showing maintenance page (AUTOMATIC mode)`
);
}
}
}

View File

@@ -27,18 +27,7 @@ export async function verifyValidSubscription(
return next();
}
const orgId = req.params.orgId || req.body.orgId || req.query.orgId || req.userOrgId;
if (!orgId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Organization ID is required to verify subscription"
)
);
}
const tier = await getOrgTierData(orgId);
const tier = await getOrgTierData(req.params.orgId);
if (!tier.active) {
return next(

View File

@@ -436,18 +436,18 @@ authenticated.get(
authenticated.post(
"/re-key/:clientId/regenerate-client-secret",
verifyClientAccess, // this is first to set the org id
verifyValidLicense,
verifyValidSubscription,
verifyClientAccess,
verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateClientSecret
);
authenticated.post(
"/re-key/:siteId/regenerate-site-secret",
verifySiteAccess, // this is first to set the org id
verifyValidLicense,
verifyValidSubscription,
verifySiteAccess,
verifyUserHasAction(ActionsEnum.reGenerateSecret),
reKey.reGenerateSiteSecret
);

View File

@@ -18,7 +18,8 @@ import * as logs from "#private/routers/auditLogs";
import {
verifyApiKeyHasAction,
verifyApiKeyIsRoot,
verifyApiKeyOrgAccess
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess
} from "@server/middlewares";
import {
verifyValidSubscription,
@@ -31,6 +32,8 @@ import {
authenticated as a
} from "@server/routers/integration";
import { logActionAudit } from "#private/middlewares";
import config from "#private/lib/config";
import { build } from "@server/build";
export const unauthenticated = ua;
export const authenticated = a;
@@ -88,3 +91,49 @@ authenticated.get(
logActionAudit(ActionsEnum.exportLogs),
logs.exportAccessAuditLogs
);
authenticated.put(
"/org/:orgId/idp/oidc",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.createIdp),
logActionAudit(ActionsEnum.createIdp),
orgIdp.createOrgOidcIdp
);
authenticated.post(
"/org/:orgId/idp/:idpId/oidc",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess,
verifyApiKeyHasAction(ActionsEnum.updateIdp),
logActionAudit(ActionsEnum.updateIdp),
orgIdp.updateOrgOidcIdp
);
authenticated.delete(
"/org/:orgId/idp/:idpId",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess,
verifyApiKeyHasAction(ActionsEnum.deleteIdp),
logActionAudit(ActionsEnum.deleteIdp),
orgIdp.deleteOrgIdp
);
authenticated.get(
"/org/:orgId/idp/:idpId",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess,
verifyApiKeyHasAction(ActionsEnum.getIdp),
orgIdp.getOrgIdp
);
authenticated.get(
"/org/:orgId/idp",
verifyValidLicense,
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listIdps),
orgIdp.listOrgIdps
);

View File

@@ -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;
}

View File

@@ -46,22 +46,23 @@ const bodySchema = z.strictObject({
roleMapping: z.string().optional()
});
// registry.registerPath({
// method: "put",
// path: "/idp/oidc",
// description: "Create an OIDC IdP.",
// tags: [OpenAPITags.Idp],
// request: {
// body: {
// content: {
// "application/json": {
// schema: bodySchema
// }
// }
// }
// },
// responses: {}
// });
registry.registerPath({
method: "put",
path: "/org/{orgId}/idp/oidc",
description: "Create an OIDC IdP for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
export async function createOrgOidcIdp(
req: Request,

View File

@@ -32,9 +32,9 @@ const paramsSchema = z
registry.registerPath({
method: "delete",
path: "/idp/{idpId}",
description: "Delete IDP.",
tags: [OpenAPITags.Idp],
path: "/org/{orgId}/idp/{idpId}",
description: "Delete IDP for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: {
params: paramsSchema
},

View File

@@ -48,16 +48,16 @@ async function query(idpId: number, orgId: string) {
return res;
}
// registry.registerPath({
// method: "get",
// path: "/idp/{idpId}",
// description: "Get an IDP by its IDP ID.",
// tags: [OpenAPITags.Idp],
// request: {
// params: paramsSchema
// },
// responses: {}
// });
registry.registerPath({
method: "get",
path: "/org/:orgId/idp/:idpId",
description: "Get an IDP by its IDP ID for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: {
params: paramsSchema
},
responses: {}
});
export async function getOrgIdp(
req: Request,

View File

@@ -62,16 +62,17 @@ async function query(orgId: string, limit: number, offset: number) {
return res;
}
// registry.registerPath({
// method: "get",
// path: "/idp",
// description: "List all IDP in the system.",
// tags: [OpenAPITags.Idp],
// request: {
// query: querySchema
// },
// responses: {}
// });
registry.registerPath({
method: "get",
path: "/org/{orgId}/idp",
description: "List all IDP for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: {
query: querySchema,
params: paramsSchema
},
responses: {}
});
export async function listOrgIdps(
req: Request,

View File

@@ -53,23 +53,23 @@ export type UpdateOrgIdpResponse = {
idpId: number;
};
// registry.registerPath({
// method: "post",
// path: "/idp/{idpId}/oidc",
// description: "Update an OIDC IdP.",
// tags: [OpenAPITags.Idp],
// request: {
// params: paramsSchema,
// body: {
// content: {
// "application/json": {
// schema: bodySchema
// }
// }
// }
// },
// responses: {}
// });
registry.registerPath({
method: "post",
path: "/org/{orgId}/idp/{idpId}/oidc",
description: "Update an OIDC IdP for a specific organization.",
tags: [OpenAPITags.Idp, OpenAPITags.Org],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
export async function updateOrgOidcIdp(
req: Request,

View File

@@ -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;
}

View File

@@ -82,7 +82,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
<Layout
orgId={params.orgId}
orgs={orgs}
navItems={orgNavSections()}
navItems={orgNavSections(env)}
>
{children}
</Layout>

View File

@@ -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

View File

@@ -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 (
<UserProvider user={user}>
<Layout orgs={orgs} navItems={adminNavSections}>
<Layout orgs={orgs} navItems={adminNavSections(env)}>
{props.children}
</Layout>
</UserProvider>

View File

@@ -7,7 +7,6 @@ import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { CheckCircle2 } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useEffect } from "react";
export default function DeviceAuthSuccessPage() {
const { env } = useEnvContext();
@@ -21,29 +20,6 @@ export default function DeviceAuthSuccessPage() {
? env.branding.logo?.authPage?.height || 58
: 58;
useEffect(() => {
// Detect if we're on iOS or Android
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
const isIOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
const isAndroid = /android/i.test(userAgent);
if (isIOS || isAndroid) {
// Wait 500ms then attempt to open the app
setTimeout(() => {
// Try to open the app using deep link
window.location.href = "pangolin://";
setTimeout(() => {
if (isIOS) {
window.location.href = "https://apps.apple.com/app/pangolin/net.pangolin.Pangolin.PangoliniOS";
} else if (isAndroid) {
window.location.href = "https://play.google.com/store/apps/details?id=net.pangolin.Pangolin";
}
}, 2000);
}, 500);
}
}, []);
return (
<>
<Card>

View File

@@ -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<AxiosResponse<ListIdpsResponse>>("/idp")
)();
@@ -121,7 +121,7 @@ export default async function Page(props: {
</p>
)}
{!isInvite && build === "saas" ? (
{!isInvite && (build === "saas" || env.flags.useOrgOnlyIdp) ? (
<div className="text-center text-muted-foreground mt-12 flex flex-col items-center">
<span>{t("needToSignInToOrg")}</span>
<Link

View File

@@ -11,6 +11,7 @@ import {
} from "@server/routers/loginPage/types";
import { redirect } from "next/navigation";
import OrgLoginPage from "@app/components/OrgLoginPage";
import { pullEnv } from "@app/lib/pullEnv";
export const dynamic = "force-dynamic";
@@ -21,7 +22,9 @@ export default async function OrgAuthPage(props: {
const searchParams = await props.searchParams;
const params = await props.params;
if (build !== "saas") {
const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
const queryString = new URLSearchParams(searchParams as any).toString();
redirect(`/auth/login${queryString ? `?${queryString}` : ""}`);
}
@@ -50,29 +53,25 @@ export default async function OrgAuthPage(props: {
} catch (e) {}
let loginIdps: LoginFormIDP[] = [];
if (build === "saas") {
const idpsRes = await priv.get<AxiosResponse<ListOrgIdpsResponse>>(
`/org/${orgId}/idp`
);
const idpsRes = await priv.get<AxiosResponse<ListOrgIdpsResponse>>(
`/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<LoadLoginPageBrandingResponse>
>(`/login-page-branding?orgId=${orgId}`);
if (res.status === 200) {
branding = res.data.data;
}
} catch (error) {}
}
try {
const res = await priv.get<
AxiosResponse<LoadLoginPageBrandingResponse>
>(`/login-page-branding?orgId=${orgId}`);
if (res.status === 200) {
branding = res.data.data;
}
} catch (error) {}
return (
<OrgLoginPage

View File

@@ -33,12 +33,12 @@ export default async function OrgAuthPage(props: {
const forceLoginParam = searchParams.forceLogin;
const forceLogin = forceLoginParam === "true";
if (build !== "saas") {
const env = pullEnv();
if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
redirect("/");
}
const env = pullEnv();
const authHeader = await authCookieHeader();
if (searchParams.token) {

View File

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

View File

@@ -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: <Server className="size-4 flex-none" />,
showEE: true
icon: <Server className="size-4 flex-none" />
}
]
: [])
@@ -123,13 +123,12 @@ export const orgNavSections = (): SidebarNavSection[] => [
href: "/{orgId}/settings/access/roles",
icon: <Users className="size-4 flex-none" />
},
...(build == "saas"
...(build == "saas" || env?.flags.useOrgOnlyIdp
? [
{
title: "sidebarIdentityProviders",
href: "/{orgId}/settings/idp",
icon: <Fingerprint className="size-4 flex-none" />,
showEE: true
icon: <Fingerprint className="size-4 flex-none" />
}
]
: []),
@@ -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: <KeyRound className="size-4 flex-none" />
},
{
title: "sidebarIdentityProviders",
href: "/admin/idp",
icon: <Fingerprint className="size-4 flex-none" />
},
...(build === "oss" || !env?.flags.useOrgOnlyIdp
? [
{
title: "sidebarIdentityProviders",
href: "/admin/idp",
icon: <Fingerprint className="size-4 flex-none" />
}
]
: []),
...(build == "enterprise"
? [
{

View File

@@ -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({
</div>
</div>
{build === "saas" && (
{build === "saas" ||
env.env.flags.useOrgOnlyIdp ? (
<>
<div className="mt-3 mb-6">
<SettingsSectionTitle>
@@ -343,7 +345,7 @@ export default function AuthPageBrandingForm({
/>
</div>
</>
)}
) : null}
<div className="mt-3 mb-6">
<SettingsSectionTitle>

View File

@@ -63,8 +63,6 @@ export default function ConfirmDeleteDialog({
}
});
const isConfirmed = form.watch("string") === string;
async function onSubmit() {
try {
await onConfirm();
@@ -141,8 +139,7 @@ export default function ConfirmDeleteDialog({
type="submit"
form="confirm-delete-form"
loading={loading}
disabled={loading || !isConfirmed}
className={!isConfirmed && !loading ? "opacity-50" : ""}
disabled={loading}
>
{buttonText}
</Button>

View File

@@ -114,6 +114,16 @@ function getActionsCategories(root: boolean) {
}
};
if (root || build === "saas" || env.flags.useOrgOnlyIdp) {
actionsByCategory["Identity Provider (IDP)"] = {
[t("actionCreateIdp")]: "createIdp",
[t("actionUpdateIdp")]: "updateIdp",
[t("actionDeleteIdp")]: "deleteIdp",
[t("actionListIdps")]: "listIdps",
[t("actionGetIdp")]: "getIdp"
};
}
if (root) {
actionsByCategory["Organization"] = {
[t("actionListOrgs")]: "listOrgs",
@@ -128,24 +138,21 @@ function getActionsCategories(root: boolean) {
...actionsByCategory["Organization"]
};
actionsByCategory["Identity Provider (IDP)"] = {
[t("actionCreateIdp")]: "createIdp",
[t("actionUpdateIdp")]: "updateIdp",
[t("actionDeleteIdp")]: "deleteIdp",
[t("actionListIdps")]: "listIdps",
[t("actionGetIdp")]: "getIdp",
[t("actionCreateIdpOrg")]: "createIdpOrg",
[t("actionDeleteIdpOrg")]: "deleteIdpOrg",
[t("actionListIdpOrgs")]: "listIdpOrgs",
[t("actionUpdateIdpOrg")]: "updateIdpOrg"
};
actionsByCategory["Identity Provider (IDP)"][t("actionCreateIdpOrg")] =
"createIdpOrg";
actionsByCategory["Identity Provider (IDP)"][t("actionDeleteIdpOrg")] =
"deleteIdpOrg";
actionsByCategory["Identity Provider (IDP)"][t("actionListIdpOrgs")] =
"listIdpOrgs";
actionsByCategory["Identity Provider (IDP)"][t("actionUpdateIdpOrg")] =
"updateIdpOrg";
actionsByCategory["User"] = {
[t("actionUpdateUser")]: "updateUser",
[t("actionGetUser")]: "getUser"
};
if (build == "saas") {
if (build === "saas") {
actionsByCategory["SAAS"] = {
["Send Usage Notification Email"]: "sendUsageNotification"
};

View File

@@ -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: {

View File

@@ -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;
}
})

View File

@@ -34,6 +34,7 @@ export type Env = {
hideSupporterKey: boolean;
usePangolinDns: boolean;
disableProductHelpBanners: boolean;
useOrgOnlyIdp: boolean;
};
branding: {
appName?: string;