Support pin,pass,whitelist correctly on login

This commit is contained in:
Owen
2026-06-01 21:32:07 -07:00
parent 8a57d8dd9c
commit b6d688f15e
12 changed files with 547 additions and 395 deletions
+10 -1
View File
@@ -19,6 +19,9 @@ export async function createResourceSession(opts: {
userSessionId?: string | null; userSessionId?: string | null;
whitelistId?: number | null; whitelistId?: number | null;
accessTokenId?: string | null; accessTokenId?: string | null;
policyPasswordId?: number | null;
policyPincodeId?: number | null;
policyWhitelistId?: number | null;
doNotExtend?: boolean; doNotExtend?: boolean;
expiresAt?: number | null; expiresAt?: number | null;
sessionLength?: number | null; sessionLength?: number | null;
@@ -28,7 +31,10 @@ export async function createResourceSession(opts: {
!opts.pincodeId && !opts.pincodeId &&
!opts.whitelistId && !opts.whitelistId &&
!opts.accessTokenId && !opts.accessTokenId &&
!opts.userSessionId !opts.userSessionId &&
!opts.policyPasswordId &&
!opts.policyPincodeId &&
!opts.policyWhitelistId
) { ) {
throw new Error("Auth method must be provided"); throw new Error("Auth method must be provided");
} }
@@ -49,6 +55,9 @@ export async function createResourceSession(opts: {
whitelistId: opts.whitelistId || null, whitelistId: opts.whitelistId || null,
doNotExtend: opts.doNotExtend || false, doNotExtend: opts.doNotExtend || false,
accessTokenId: opts.accessTokenId || null, accessTokenId: opts.accessTokenId || null,
policyPasswordId: opts.policyPasswordId || null,
policyPincodeId: opts.policyPincodeId || null,
policyWhitelistId: opts.policyWhitelistId || null,
isRequestToken: opts.isRequestToken || false, isRequestToken: opts.isRequestToken || false,
userSessionId: opts.userSessionId || null, userSessionId: opts.userSessionId || null,
issuedAt: new Date().getTime() issuedAt: new Date().getTime()
+18
View File
@@ -820,6 +820,24 @@ export const resourceSessions = pgTable("resourceSessions", {
onDelete: "cascade" onDelete: "cascade"
} }
), ),
policyPasswordId: integer("policyPasswordId").references(
() => resourcePolicyPassword.passwordId,
{
onDelete: "cascade"
}
),
policyPincodeId: integer("policyPincodeId").references(
() => resourcePolicyPincode.pincodeId,
{
onDelete: "cascade"
}
),
policyWhitelistId: integer("policyWhitelistId").references(
() => resourcePolicyWhiteList.whitelistId,
{
onDelete: "cascade"
}
),
issuedAt: bigint("issuedAt", { mode: "number" }) issuedAt: bigint("issuedAt", { mode: "number" })
}); });
+18
View File
@@ -1148,6 +1148,24 @@ export const resourceSessions = sqliteTable("resourceSessions", {
onDelete: "cascade" onDelete: "cascade"
} }
), ),
policyPasswordId: integer("policyPasswordId").references(
() => resourcePolicyPassword.passwordId,
{
onDelete: "cascade"
}
),
policyPincodeId: integer("policyPincodeId").references(
() => resourcePolicyPincode.pincodeId,
{
onDelete: "cascade"
}
),
policyWhitelistId: integer("policyWhitelistId").references(
() => resourcePolicyWhiteList.whitelistId,
{
onDelete: "cascade"
}
),
issuedAt: integer("issuedAt") issuedAt: integer("issuedAt")
}); });
+2 -1
View File
@@ -520,7 +520,8 @@ export class TraefikConfigManager {
build != "oss", // generate the login pages on the cloud and hybrid, build != "oss", // generate the login pages on the cloud and hybrid,
build == "saas" build == "saas"
? false ? false
: config.getRawConfig().traefik.allow_raw_resources // dont allow raw resources on saas otherwise use config : config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config
build != "oss" // generate browser gateway targets on cloud and enterprise
); );
const domains = new Set<string>(); const domains = new Set<string>();
+321 -304
View File
@@ -85,7 +85,8 @@ export async function getTraefikConfig(
filterOutNamespaceDomains = false, filterOutNamespaceDomains = false,
generateLoginPageRouters = false, generateLoginPageRouters = false,
allowRawResources = true, allowRawResources = true,
allowMaintenancePage = true allowMaintenancePage = true,
allowBrowserGatewayResources = true
): Promise<any> { ): Promise<any> {
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
@@ -276,64 +277,6 @@ export async function getTraefikConfig(
}); });
}); });
// Query browser gateway targets for this exit node
const browserGatewayRows = await db
.select({
// Resource fields
resourceId: resources.resourceId,
resourceName: resources.name,
fullDomain: resources.fullDomain,
ssl: resources.ssl,
subdomain: resources.subdomain,
domainId: resources.domainId,
enabled: resources.enabled,
wildcard: resources.wildcard,
domainCertResolver: domains.certResolver,
preferWildcardCert: domains.preferWildcardCert,
domainNamespaceId: domainNamespaces.domainNamespaceId,
// Maintenance fields
maintenanceModeEnabled: resources.maintenanceModeEnabled,
maintenanceModeType: resources.maintenanceModeType,
maintenanceTitle: resources.maintenanceTitle,
maintenanceMessage: resources.maintenanceMessage,
maintenanceEstimatedTime: resources.maintenanceEstimatedTime,
// Browser gateway target fields
browserGatewayTargetId: browserGatewayTarget.browserGatewayTargetId,
bgType: browserGatewayTarget.type,
// Site fields
siteId: sites.siteId,
siteType: sites.type,
siteOnline: sites.online,
subnet: sites.subnet,
siteExitNodeId: sites.exitNodeId
})
.from(browserGatewayTarget)
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
.innerJoin(
resources,
eq(resources.resourceId, browserGatewayTarget.resourceId)
)
.leftJoin(domains, eq(domains.domainId, resources.domainId))
.leftJoin(
domainNamespaces,
eq(domainNamespaces.domainId, resources.domainId)
)
.where(
and(
eq(resources.enabled, true),
or(
eq(sites.exitNodeId, exitNodeId),
and(
isNull(sites.exitNodeId),
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)`,
eq(sites.type, "local"),
sql`(${build != "saas" ? 1 : 0} = 1)`
)
),
inArray(sites.type, siteTypes)
)
);
// Group browser gateway targets by resource // Group browser gateway targets by resource
type BrowserGatewayResourceEntry = { type BrowserGatewayResourceEntry = {
resourceId: number; resourceId: number;
@@ -366,39 +309,100 @@ export async function getTraefikConfig(
BrowserGatewayResourceEntry BrowserGatewayResourceEntry
>(); >();
for (const row of browserGatewayRows) { if (allowBrowserGatewayResources) {
if (filterOutNamespaceDomains && row.domainNamespaceId) { // Query browser gateway targets for this exit node
continue; const browserGatewayRows = await db
} .select({
if (!browserGatewayResourcesMap.has(row.resourceId)) { // Resource fields
browserGatewayResourcesMap.set(row.resourceId, { resourceId: resources.resourceId,
resourceId: row.resourceId, resourceName: resources.name,
name: sanitize(row.resourceName) || "", fullDomain: resources.fullDomain,
fullDomain: row.fullDomain, ssl: resources.ssl,
ssl: row.ssl, subdomain: resources.subdomain,
subdomain: row.subdomain, domainId: resources.domainId,
domainId: row.domainId, enabled: resources.enabled,
enabled: row.enabled, wildcard: resources.wildcard,
wildcard: row.wildcard, domainCertResolver: domains.certResolver,
domainCertResolver: row.domainCertResolver, preferWildcardCert: domains.preferWildcardCert,
preferWildcardCert: row.preferWildcardCert, domainNamespaceId: domainNamespaces.domainNamespaceId,
maintenanceModeEnabled: row.maintenanceModeEnabled, // Maintenance fields
maintenanceModeType: row.maintenanceModeType, maintenanceModeEnabled: resources.maintenanceModeEnabled,
maintenanceTitle: row.maintenanceTitle, maintenanceModeType: resources.maintenanceModeType,
maintenanceMessage: row.maintenanceMessage, maintenanceTitle: resources.maintenanceTitle,
maintenanceEstimatedTime: row.maintenanceEstimatedTime, maintenanceMessage: resources.maintenanceMessage,
targets: [] maintenanceEstimatedTime: resources.maintenanceEstimatedTime,
// Browser gateway target fields
browserGatewayTargetId:
browserGatewayTarget.browserGatewayTargetId,
bgType: browserGatewayTarget.type,
// Site fields
siteId: sites.siteId,
siteType: sites.type,
siteOnline: sites.online,
subnet: sites.subnet,
siteExitNodeId: sites.exitNodeId
})
.from(browserGatewayTarget)
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
.innerJoin(
resources,
eq(resources.resourceId, browserGatewayTarget.resourceId)
)
.leftJoin(domains, eq(domains.domainId, resources.domainId))
.leftJoin(
domainNamespaces,
eq(domainNamespaces.domainId, resources.domainId)
)
.where(
and(
eq(resources.enabled, true),
or(
eq(sites.exitNodeId, exitNodeId),
and(
isNull(sites.exitNodeId),
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)`,
eq(sites.type, "local"),
sql`(${build != "saas" ? 1 : 0} = 1)`
)
),
inArray(sites.type, siteTypes)
)
);
for (const row of browserGatewayRows) {
if (filterOutNamespaceDomains && row.domainNamespaceId) {
continue;
}
if (!browserGatewayResourcesMap.has(row.resourceId)) {
browserGatewayResourcesMap.set(row.resourceId, {
resourceId: row.resourceId,
name: sanitize(row.resourceName) || "",
fullDomain: row.fullDomain,
ssl: row.ssl,
subdomain: row.subdomain,
domainId: row.domainId,
enabled: row.enabled,
wildcard: row.wildcard,
domainCertResolver: row.domainCertResolver,
preferWildcardCert: row.preferWildcardCert,
maintenanceModeEnabled: row.maintenanceModeEnabled,
maintenanceModeType: row.maintenanceModeType,
maintenanceTitle: row.maintenanceTitle,
maintenanceMessage: row.maintenanceMessage,
maintenanceEstimatedTime: row.maintenanceEstimatedTime,
targets: []
});
}
browserGatewayResourcesMap.get(row.resourceId)!.targets.push({
browserGatewayTargetId: row.browserGatewayTargetId,
bgType: row.bgType,
siteId: row.siteId,
siteType: row.siteType,
siteOnline: row.siteOnline,
subnet: row.subnet,
siteExitNodeId: row.siteExitNodeId
}); });
} }
browserGatewayResourcesMap.get(row.resourceId)!.targets.push({
browserGatewayTargetId: row.browserGatewayTargetId,
bgType: row.bgType,
siteId: row.siteId,
siteType: row.siteType,
siteOnline: row.siteOnline,
subnet: row.subnet,
siteExitNodeId: row.siteExitNodeId
});
} }
let siteResourcesWithFullDomain: { let siteResourcesWithFullDomain: {
@@ -1055,245 +1059,257 @@ export async function getTraefikConfig(
} }
} }
// Generate Traefik config for browser gateway resources if (allowBrowserGatewayResources) {
const browserGatewayPort = 39999; // Generate Traefik config for browser gateway resources
for (const [, bgResource] of browserGatewayResourcesMap.entries()) { const browserGatewayPort = 39999;
if (!bgResource.enabled) continue; for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
if (!bgResource.domainId) continue; if (!bgResource.enabled) continue;
if (!bgResource.fullDomain) continue; if (!bgResource.domainId) continue;
if (!bgResource.fullDomain) continue;
if (!config_output.http.routers) config_output.http.routers = {}; if (!config_output.http.routers) config_output.http.routers = {};
if (!config_output.http.services) config_output.http.services = {}; if (!config_output.http.services) config_output.http.services = {};
const fullDomain = bgResource.fullDomain; const fullDomain = bgResource.fullDomain;
const additionalMiddlewares = const additionalMiddlewares =
config.getRawConfig().traefik.additional_middlewares || []; config.getRawConfig().traefik.additional_middlewares || [];
const routerMiddlewares = [ const routerMiddlewares = [
badgerMiddlewareName, badgerMiddlewareName,
...additionalMiddlewares ...additionalMiddlewares
]; ];
const hostRule = `Host(\`${fullDomain}\`)`; const hostRule = `Host(\`${fullDomain}\`)`;
// Build TLS config // Build TLS config
let tls = {}; let tls = {};
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
const domainParts = fullDomain.split("."); const domainParts = fullDomain.split(".");
let wildCard: string; let wildCard: string;
if (domainParts.length <= 2) { if (domainParts.length <= 2) {
wildCard = `*.${domainParts.join(".")}`; wildCard = `*.${domainParts.join(".")}`;
} else {
wildCard = `*.${domainParts.slice(1).join(".")}`;
}
if (!bgResource.subdomain) {
wildCard = fullDomain;
}
const globalDefaultResolver =
config.getRawConfig().traefik.cert_resolver;
const globalDefaultPreferWildcard =
config.getRawConfig().traefik.prefer_wildcard_cert;
const resolverName = bgResource.domainCertResolver
? bgResource.domainCertResolver.trim()
: globalDefaultResolver;
const preferWildcard =
bgResource.preferWildcardCert !== undefined &&
bgResource.preferWildcardCert !== null
? bgResource.preferWildcardCert
: globalDefaultPreferWildcard;
tls = {
certResolver: resolverName,
...(preferWildcard ? { domains: [{ main: wildCard }] } : {})
};
} else { } else {
wildCard = `*.${domainParts.slice(1).join(".")}`; const matchingCert = validCerts.find(
} (cert) => cert.queriedDomain === fullDomain
if (!bgResource.subdomain) {
wildCard = fullDomain;
}
const globalDefaultResolver =
config.getRawConfig().traefik.cert_resolver;
const globalDefaultPreferWildcard =
config.getRawConfig().traefik.prefer_wildcard_cert;
const resolverName = bgResource.domainCertResolver
? bgResource.domainCertResolver.trim()
: globalDefaultResolver;
const preferWildcard =
bgResource.preferWildcardCert !== undefined &&
bgResource.preferWildcardCert !== null
? bgResource.preferWildcardCert
: globalDefaultPreferWildcard;
tls = {
certResolver: resolverName,
...(preferWildcard ? { domains: [{ main: wildCard }] } : {})
};
} else {
const matchingCert = validCerts.find(
(cert) => cert.queriedDomain === fullDomain
);
if (!matchingCert) {
logger.debug(
`No matching certificate found for browser gateway domain: ${fullDomain}`
); );
if (!matchingCert) {
logger.debug(
`No matching certificate found for browser gateway domain: ${fullDomain}`
);
continue;
}
}
const bgUiServiceName = `bg-r${bgResource.resourceId}-ui-service`;
if (bgResource.ssl) {
const redirectRouterName = `bg-r${bgResource.resourceId}-redirect`;
config_output.http.routers![redirectRouterName] = {
entryPoints: [
config.getRawConfig().traefik.http_entrypoint
],
middlewares: [redirectHttpsMiddlewareName],
service: bgUiServiceName,
rule: hostRule,
priority: 100
};
}
// Collect online sites for this resource (for any type)
const anySiteOnline = bgResource.targets.some((t) => t.siteOnline);
// Maintenance page logic for browser gateway resources
let showBgMaintenancePage = false;
if (bgResource.maintenanceModeEnabled) {
if (bgResource.maintenanceModeType === "forced") {
showBgMaintenancePage = true;
} else if (bgResource.maintenanceModeType === "automatic") {
showBgMaintenancePage = !anySiteOnline;
}
}
if (showBgMaintenancePage && allowMaintenancePage) {
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`;
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`;
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`;
const entrypointHttp =
config.getRawConfig().traefik.http_entrypoint;
const entrypointHttps =
config.getRawConfig().traefik.https_entrypoint;
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
if (!config_output.http.services)
config_output.http.services = {};
if (!config_output.http.middlewares)
config_output.http.middlewares = {};
if (!config_output.http.routers)
config_output.http.routers = {};
config_output.http.services![bgMaintenanceServiceName] = {
loadBalancer: {
servers: [
{
url: `http://${maintenanceHost}:${maintenancePort}`
}
],
passHostHeader: true
}
};
config_output.http.middlewares![bgRewriteMiddlewareName] = {
replacePathRegex: {
regex: "^/(.*)",
replacement: "/maintenance-screen"
}
};
config_output.http.routers![bgMaintenanceRouterName] = {
entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
middlewares: [bgRewriteMiddlewareName],
rule: hostRule,
priority: 2000,
...(bgResource.ssl ? { tls } : {})
};
config_output.http.routers![
`${bgMaintenanceRouterName}-assets`
] = {
entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 2001,
...(bgResource.ssl ? { tls } : {})
};
continue; continue;
} }
}
const bgUiServiceName = `bg-r${bgResource.resourceId}-ui-service`; // Group targets by type and generate per-type websocket routers and services
const typeMap = new Map<string, typeof bgResource.targets>();
if (bgResource.ssl) { for (const t of bgResource.targets) {
const redirectRouterName = `bg-r${bgResource.resourceId}-redirect`; if (!typeMap.has(t.bgType)) typeMap.set(t.bgType, []);
config_output.http.routers![redirectRouterName] = { typeMap.get(t.bgType)!.push(t);
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
middlewares: [redirectHttpsMiddlewareName],
service: bgUiServiceName,
rule: hostRule,
priority: 100
};
}
// Collect online sites for this resource (for any type)
const anySiteOnline = bgResource.targets.some((t) => t.siteOnline);
// Maintenance page logic for browser gateway resources
let showBgMaintenancePage = false;
if (bgResource.maintenanceModeEnabled) {
if (bgResource.maintenanceModeType === "forced") {
showBgMaintenancePage = true;
} else if (bgResource.maintenanceModeType === "automatic") {
showBgMaintenancePage = !anySiteOnline;
} }
}
if (showBgMaintenancePage && allowMaintenancePage) { for (const [bgType, typedTargets] of typeMap.entries()) {
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`; const bgKey = `bg-r${bgResource.resourceId}-${bgType}`;
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`; const bgRouterName = `${bgKey}-router`;
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`; const bgServiceName = `${bgKey}-service`;
const bgRule = `${hostRule} && PathPrefix(\`/gateway/${bgType}\`)`;
const entrypointHttp = const servers = typedTargets
config.getRawConfig().traefik.http_entrypoint; .filter((t) => {
const entrypointHttps = if (!t.siteOnline && anySiteOnline) return false;
config.getRawConfig().traefik.https_entrypoint; if (t.siteType === "newt") return !!t.subnet;
return false; // browser gateway only supported on newt sites
})
.map((t) => ({
url: `http://${t.subnet!.split("/")[0]}:${browserGatewayPort}`
}))
.filter(
(v, i, a) => a.findIndex((u) => u.url === v.url) === i
);
const maintenancePort = config.getRawConfig().server.next_port; config_output.http.routers![bgRouterName] = {
const maintenanceHost = entryPoints: [
config.getRawConfig().server.internal_hostname; bgResource.ssl
? config.getRawConfig().traefik.https_entrypoint
if (!config_output.http.services) config_output.http.services = {}; : config.getRawConfig().traefik.http_entrypoint
if (!config_output.http.middlewares)
config_output.http.middlewares = {};
if (!config_output.http.routers) config_output.http.routers = {};
config_output.http.services![bgMaintenanceServiceName] = {
loadBalancer: {
servers: [
{ url: `http://${maintenanceHost}:${maintenancePort}` }
], ],
passHostHeader: true middlewares: routerMiddlewares,
} service: bgServiceName,
}; rule: bgRule,
priority: 110, // highest - websocket path takes precedence
...(bgResource.ssl ? { tls } : {})
};
config_output.http.middlewares![bgRewriteMiddlewareName] = { config_output.http.services![bgServiceName] = {
loadBalancer: {
servers
}
};
}
// UI: serve the browser gateway page from the internal pangolin instance.
// The primary type is used for the path rewrite (e.g. /rdp), mirroring
// how the maintenance page rewrites everything to /maintenance-screen.
const primaryType = typeMap.keys().next().value as string;
const internalHost = config.getRawConfig().server.internal_hostname;
const internalPort = config.getRawConfig().server.next_port;
const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`;
const entrypoint = bgResource.ssl
? config.getRawConfig().traefik.https_entrypoint
: config.getRawConfig().traefik.http_entrypoint;
if (!config_output.http.middlewares) {
config_output.http.middlewares = {};
}
config_output.http.middlewares![uiRewriteMiddlewareName] = {
replacePathRegex: { replacePathRegex: {
regex: "^/(.*)", regex: "^/(.*)",
replacement: "/maintenance-screen" replacement: `/${primaryType}`
} }
}; };
config_output.http.routers![bgMaintenanceRouterName] = { config_output.http.services![bgUiServiceName] = {
entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
middlewares: [bgRewriteMiddlewareName],
rule: hostRule,
priority: 2000,
...(bgResource.ssl ? { tls } : {})
};
config_output.http.routers![`${bgMaintenanceRouterName}-assets`] = {
entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 2001,
...(bgResource.ssl ? { tls } : {})
};
continue;
}
// Group targets by type and generate per-type websocket routers and services
const typeMap = new Map<string, typeof bgResource.targets>();
for (const t of bgResource.targets) {
if (!typeMap.has(t.bgType)) typeMap.set(t.bgType, []);
typeMap.get(t.bgType)!.push(t);
}
for (const [bgType, typedTargets] of typeMap.entries()) {
const bgKey = `bg-r${bgResource.resourceId}-${bgType}`;
const bgRouterName = `${bgKey}-router`;
const bgServiceName = `${bgKey}-service`;
const bgRule = `${hostRule} && PathPrefix(\`/gateway/${bgType}\`)`;
const servers = typedTargets
.filter((t) => {
if (!t.siteOnline && anySiteOnline) return false;
if (t.siteType === "newt") return !!t.subnet;
return false; // browser gateway only supported on newt sites
})
.map((t) => ({
url: `http://${t.subnet!.split("/")[0]}:${browserGatewayPort}`
}))
.filter((v, i, a) => a.findIndex((u) => u.url === v.url) === i);
config_output.http.routers![bgRouterName] = {
entryPoints: [
bgResource.ssl
? config.getRawConfig().traefik.https_entrypoint
: config.getRawConfig().traefik.http_entrypoint
],
middlewares: routerMiddlewares,
service: bgServiceName,
rule: bgRule,
priority: 110, // highest - websocket path takes precedence
...(bgResource.ssl ? { tls } : {})
};
config_output.http.services![bgServiceName] = {
loadBalancer: { loadBalancer: {
servers servers: [
{
url: `http://${internalHost}:${internalPort}`
}
]
} }
}; };
}
// UI: serve the browser gateway page from the internal pangolin instance. // Assets router at higher priority so /_next files load without rewrite
// The primary type is used for the path rewrite (e.g. /rdp), mirroring config_output.http.routers![
// how the maintenance page rewrites everything to /maintenance-screen. `bg-r${bgResource.resourceId}-assets-router`
const primaryType = typeMap.keys().next().value as string; ] = {
const internalHost = config.getRawConfig().server.internal_hostname; entryPoints: [entrypoint],
const internalPort = config.getRawConfig().server.next_port; middlewares: routerMiddlewares,
const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`; service: bgUiServiceName,
const entrypoint = bgResource.ssl rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
? config.getRawConfig().traefik.https_entrypoint priority: 101,
: config.getRawConfig().traefik.http_entrypoint; ...(bgResource.ssl ? { tls } : {})
};
if (!config_output.http.middlewares) { // Catch-all router rewrites everything on the domain to /{primaryType}
config_output.http.middlewares = {}; config_output.http.routers![
} `bg-r${bgResource.resourceId}-ui-router`
] = {
config_output.http.middlewares![uiRewriteMiddlewareName] = {
replacePathRegex: {
regex: "^/(.*)",
replacement: `/${primaryType}`
}
};
config_output.http.services![bgUiServiceName] = {
loadBalancer: {
servers: [
{
url: `http://${internalHost}:${internalPort}`
}
]
}
};
// Assets router at higher priority so /_next files load without rewrite
config_output.http.routers![
`bg-r${bgResource.resourceId}-assets-router`
] = {
entryPoints: [entrypoint],
middlewares: routerMiddlewares,
service: bgUiServiceName,
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 101,
...(bgResource.ssl ? { tls } : {})
};
// Catch-all router rewrites everything on the domain to /{primaryType}
config_output.http.routers![`bg-r${bgResource.resourceId}-ui-router`] =
{
entryPoints: [entrypoint], entryPoints: [entrypoint],
middlewares: [...routerMiddlewares, uiRewriteMiddlewareName], middlewares: [...routerMiddlewares, uiRewriteMiddlewareName],
service: bgUiServiceName, service: bgUiServiceName,
@@ -1301,6 +1317,7 @@ export async function getTraefikConfig(
priority: 100, priority: 100,
...(bgResource.ssl ? { tls } : {}) ...(bgResource.ssl ? { tls } : {})
}; };
}
} }
// Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that // Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that
+2 -1
View File
@@ -270,7 +270,8 @@ hybridRouter.get(
true, // But don't allow domain namespace resources true, // But don't allow domain namespace resources
false, // Dont include login pages, false, // Dont include login pages,
true, // allow raw resources true, // allow raw resources
false // dont generate maintenance page false, // dont generate maintenance page
false // dont generate browser gateway targets
); );
return response(res, { return response(res, {
+18 -8
View File
@@ -1,7 +1,7 @@
import { verify } from "@node-rs/argon2"; import { verify } from "@node-rs/argon2";
import { generateSessionToken } from "@server/auth/sessions/app"; import { generateSessionToken } from "@server/auth/sessions/app";
import { db } from "@server/db"; import { db } from "@server/db";
import { orgs, resourcePassword, resources } from "@server/db"; import { orgs, resourcePassword, resourcePolicies, resourcePolicyPassword, resources } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -61,17 +61,29 @@ export async function authWithPassword(
const [result] = await db const [result] = await db
.select() .select()
.from(resources) .from(resources)
.leftJoin(orgs, eq(orgs.orgId, resources.orgId))
.leftJoin(
resourcePolicies,
eq(resourcePolicies.resourcePolicyId, resources.resourcePolicyId)
)
.leftJoin(
resourcePolicyPassword,
eq(resourcePolicyPassword.resourcePolicyId, resourcePolicies.resourcePolicyId)
)
.leftJoin( .leftJoin(
resourcePassword, resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId) eq(resourcePassword.resourceId, resources.resourceId)
) )
.leftJoin(orgs, eq(orgs.orgId, resources.orgId))
.where(eq(resources.resourceId, resourceId)) .where(eq(resources.resourceId, resourceId))
.limit(1); .limit(1);
const resource = result?.resources; const resource = result?.resources;
const org = result?.orgs; const org = result?.orgs;
const definedPassword = result?.resourcePassword;
// Policy password takes precedence over resource-level password
const policyPassword = result?.resourcePolicyPassword ?? null;
const definedPassword = policyPassword ?? result?.resourcePassword ?? null;
const isPolicyPassword = !!policyPassword;
if (!org) { if (!org) {
return next( return next(
@@ -89,10 +101,7 @@ export async function authWithPassword(
return next( return next(
createHttpError( createHttpError(
HttpCode.UNAUTHORIZED, HttpCode.UNAUTHORIZED,
createHttpError( "Resource has no password protection"
HttpCode.BAD_REQUEST,
"Resource has no password protection"
)
) )
); );
} }
@@ -126,7 +135,8 @@ export async function authWithPassword(
await createResourceSession({ await createResourceSession({
resourceId, resourceId,
token, token,
passwordId: definedPassword.passwordId, passwordId: isPolicyPassword ? null : definedPassword.passwordId,
policyPasswordId: isPolicyPassword ? definedPassword.passwordId : null,
isRequestToken: true, isRequestToken: true,
expiresAt: Date.now() + 1000 * 30, // 30 seconds expiresAt: Date.now() + 1000 * 30, // 30 seconds
sessionLength: 1000 * 30, sessionLength: 1000 * 30,
+17 -4
View File
@@ -1,6 +1,6 @@
import { generateSessionToken } from "@server/auth/sessions/app"; import { generateSessionToken } from "@server/auth/sessions/app";
import { db } from "@server/db"; import { db } from "@server/db";
import { orgs, resourcePincode, resources } from "@server/db"; import { orgs, resourcePincode, resourcePolicies, resourcePolicyPincode, resources } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -60,17 +60,29 @@ export async function authWithPincode(
const [result] = await db const [result] = await db
.select() .select()
.from(resources) .from(resources)
.leftJoin(orgs, eq(orgs.orgId, resources.orgId))
.leftJoin(
resourcePolicies,
eq(resourcePolicies.resourcePolicyId, resources.resourcePolicyId)
)
.leftJoin(
resourcePolicyPincode,
eq(resourcePolicyPincode.resourcePolicyId, resourcePolicies.resourcePolicyId)
)
.leftJoin( .leftJoin(
resourcePincode, resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId) eq(resourcePincode.resourceId, resources.resourceId)
) )
.leftJoin(orgs, eq(orgs.orgId, resources.orgId))
.where(eq(resources.resourceId, resourceId)) .where(eq(resources.resourceId, resourceId))
.limit(1); .limit(1);
const resource = result?.resources; const resource = result?.resources;
const org = result?.orgs; const org = result?.orgs;
const definedPincode = result?.resourcePincode;
// Policy pincode takes precedence over resource-level pincode
const policyPincode = result?.resourcePolicyPincode ?? null;
const definedPincode = policyPincode ?? result?.resourcePincode ?? null;
const isPolicyPincode = !!policyPincode;
if (!org) { if (!org) {
return next( return next(
@@ -125,7 +137,8 @@ export async function authWithPincode(
await createResourceSession({ await createResourceSession({
resourceId, resourceId,
token, token,
pincodeId: definedPincode.pincodeId, pincodeId: isPolicyPincode ? null : definedPincode.pincodeId,
policyPincodeId: isPolicyPincode ? definedPincode.pincodeId : null,
isRequestToken: true, isRequestToken: true,
expiresAt: Date.now() + 1000 * 30, // 30 seconds expiresAt: Date.now() + 1000 * 30, // 30 seconds
sessionLength: 1000 * 30, sessionLength: 1000 * 30,
+106 -75
View File
@@ -1,6 +1,6 @@
import { generateSessionToken } from "@server/auth/sessions/app"; import { generateSessionToken } from "@server/auth/sessions/app";
import { db } from "@server/db"; import { db } from "@server/db";
import { orgs, resourceOtp, resources, resourceWhitelist } from "@server/db"; import { orgs, resourceOtp, resources, resourceWhitelist, resourcePolicyWhiteList } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
@@ -59,82 +59,21 @@ export async function authWithWhitelist(
const { email, otp } = parsedBody.data; const { email, otp } = parsedBody.data;
try { try {
const [result] = await db // Fetch resource and org first
const [resourceResult] = await db
.select() .select()
.from(resourceWhitelist) .from(resources)
.where(
and(
eq(resourceWhitelist.resourceId, resourceId),
eq(resourceWhitelist.email, email)
)
)
.leftJoin(
resources,
eq(resources.resourceId, resourceWhitelist.resourceId)
)
.leftJoin(orgs, eq(orgs.orgId, resources.orgId)) .leftJoin(orgs, eq(orgs.orgId, resources.orgId))
.where(eq(resources.resourceId, resourceId))
.limit(1); .limit(1);
let resource = result?.resources; const resource = resourceResult?.resources;
let org = result?.orgs; const org = resourceResult?.orgs;
let whitelistedEmail = result?.resourceWhitelist;
if (!whitelistedEmail) { if (!resource) {
// if email is not found, check for wildcard email return next(
const wildcard = "*@" + email.split("@")[1]; createHttpError(HttpCode.BAD_REQUEST, "Resource does not exist")
);
logger.debug("Checking for wildcard email: " + wildcard);
const [result] = await db
.select()
.from(resourceWhitelist)
.where(
and(
eq(resourceWhitelist.resourceId, resourceId),
eq(resourceWhitelist.email, wildcard)
)
)
.leftJoin(
resources,
eq(resources.resourceId, resourceWhitelist.resourceId)
)
.leftJoin(orgs, eq(orgs.orgId, resources.orgId))
.limit(1);
resource = result?.resources;
org = result?.orgs;
whitelistedEmail = result?.resourceWhitelist;
// if wildcard is still not found, return unauthorized
if (!whitelistedEmail) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Email is not whitelisted. Email: ${email}. IP: ${req.ip}.`
);
}
if (org && resource) {
logAccessAudit({
orgId: org.orgId,
resourceId: resource.resourceId,
action: false,
type: "whitelistedEmail",
metadata: { email },
userAgent: req.headers["user-agent"],
requestIp: req.ip
});
}
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
createHttpError(
HttpCode.BAD_REQUEST,
"Email is not whitelisted"
)
)
);
}
} }
if (!org) { if (!org) {
@@ -143,9 +82,100 @@ export async function authWithWhitelist(
); );
} }
if (!resource) { const wildcard = "*@" + email.split("@")[1];
// Check policy whitelist first (policy takes precedence over resource whitelist)
let policyWhitelistEntry: { whitelistId: number; email: string } | null = null;
if (resource.resourcePolicyId) {
const [exact] = await db
.select()
.from(resourcePolicyWhiteList)
.where(
and(
eq(resourcePolicyWhiteList.resourcePolicyId, resource.resourcePolicyId),
eq(resourcePolicyWhiteList.email, email)
)
)
.limit(1);
if (exact) {
policyWhitelistEntry = exact;
} else {
logger.debug("Checking for wildcard email in policy: " + wildcard);
const [wildcardMatch] = await db
.select()
.from(resourcePolicyWhiteList)
.where(
and(
eq(resourcePolicyWhiteList.resourcePolicyId, resource.resourcePolicyId),
eq(resourcePolicyWhiteList.email, wildcard)
)
)
.limit(1);
if (wildcardMatch) policyWhitelistEntry = wildcardMatch;
}
}
// Fall back to resource whitelist if not found in policy
let resourceWhitelistEntry: { whitelistId: number; email: string } | null = null;
if (!policyWhitelistEntry) {
const [exact] = await db
.select()
.from(resourceWhitelist)
.where(
and(
eq(resourceWhitelist.resourceId, resourceId),
eq(resourceWhitelist.email, email)
)
)
.limit(1);
if (exact) {
resourceWhitelistEntry = exact;
} else {
logger.debug("Checking for wildcard email: " + wildcard);
const [wildcardMatch] = await db
.select()
.from(resourceWhitelist)
.where(
and(
eq(resourceWhitelist.resourceId, resourceId),
eq(resourceWhitelist.email, wildcard)
)
)
.limit(1);
if (wildcardMatch) resourceWhitelistEntry = wildcardMatch;
}
}
const isPolicyWhitelist = !!policyWhitelistEntry;
const whitelistedEmail = policyWhitelistEntry ?? resourceWhitelistEntry;
if (!whitelistedEmail) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Email is not whitelisted. Email: ${email}. IP: ${req.ip}.`
);
}
logAccessAudit({
orgId: org.orgId,
resourceId: resource.resourceId,
action: false,
type: "whitelistedEmail",
metadata: { email },
userAgent: req.headers["user-agent"],
requestIp: req.ip
});
return next( return next(
createHttpError(HttpCode.BAD_REQUEST, "Resource does not exist") createHttpError(
HttpCode.UNAUTHORIZED,
createHttpError(
HttpCode.BAD_REQUEST,
"Email is not whitelisted"
)
)
); );
} }
@@ -211,7 +241,8 @@ export async function authWithWhitelist(
await createResourceSession({ await createResourceSession({
resourceId, resourceId,
token, token,
whitelistId: whitelistedEmail.whitelistId, whitelistId: isPolicyWhitelist ? null : whitelistedEmail.whitelistId,
policyWhitelistId: isPolicyWhitelist ? whitelistedEmail.whitelistId : null,
isRequestToken: true, isRequestToken: true,
expiresAt: Date.now() + 1000 * 30, // 30 seconds expiresAt: Date.now() + 1000 * 30, // 30 seconds
sessionLength: 1000 * 30, sessionLength: 1000 * 30,
@@ -22,7 +22,8 @@ export async function traefikConfigProvider(
config.getRawConfig().traefik.site_types, config.getRawConfig().traefik.site_types,
build == "oss", // filter out the namespace domains in open source build == "oss", // filter out the namespace domains in open source
build != "oss", // generate the login pages on the cloud and and enterprise, build != "oss", // generate the login pages on the cloud and and enterprise,
config.getRawConfig().traefik.allow_raw_resources config.getRawConfig().traefik.allow_raw_resources,
build != "oss" // generate browser gateway resources on cloud and enterprise
); );
if (traefikConfig?.http?.middlewares) { if (traefikConfig?.http?.middlewares) {
+18
View File
@@ -583,6 +583,24 @@ export default async function migration() {
DELETE FROM "resourceWhitelist" DELETE FROM "resourceWhitelist"
WHERE "resourceId" = ${resource.resourceId} WHERE "resourceId" = ${resource.resourceId}
`); `);
await db.execute(sql`
ALTER TABLE "resourceSessions" ADD COLUMN "policyPasswordId" integer;
`);
await db.execute(sql`
ALTER TABLE "resourceSessions" ADD COLUMN "policyPincodeId" integer;
`);
await db.execute(sql`
ALTER TABLE "resourceSessions" ADD COLUMN "policyWhitelistId" integer;
`);
await db.execute(sql`
ALTER TABLE "resourceSessions" ADD CONSTRAINT "resourceSessions_policyPasswordId_resourcePolicyPassword_passwordId_fk" FOREIGN KEY ("policyPasswordId") REFERENCES "public"."resourcePolicyPassword"("passwordId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "resourceSessions" ADD CONSTRAINT "resourceSessions_policyPincodeId_resourcePolicyPincode_pincodeId_fk" FOREIGN KEY ("policyPincodeId") REFERENCES "public"."resourcePolicyPincode"("pincodeId") ON DELETE cascade ON UPDATE no action;
`);
await db.execute(sql`
ALTER TABLE "resourceSessions" ADD CONSTRAINT "resourceSessions_policyWhitelistId_resourcePolicyWhitelist_id_fk" FOREIGN KEY ("policyWhitelistId") REFERENCES "public"."resourcePolicyWhitelist"("id") ON DELETE cascade ON UPDATE no action;
`);
} }
await db.execute(sql`COMMIT`); await db.execute(sql`COMMIT`);
+15
View File
@@ -337,6 +337,21 @@ export default async function migration() {
ALTER TABLE 'sites' ADD 'autoUpdateOverrideOrg' integer DEFAULT false NOT NULL; ALTER TABLE 'sites' ADD 'autoUpdateOverrideOrg' integer DEFAULT false NOT NULL;
` `
).run(); ).run();
db.prepare(
`
ALTER TABLE 'resourceSessions' ADD 'policyPasswordId' integer REFERENCES resourcePolicyPassword(passwordId);
`
).run();
db.prepare(
`
ALTER TABLE 'resourceSessions' ADD 'policyPincodeId' integer REFERENCES resourcePolicyPincode(pincodeId);
`
).run();
db.prepare(
`
ALTER TABLE 'resourceSessions' ADD 'policyWhitelistId' integer REFERENCES resourcePolicyWhitelist(id);
`
).run();
})(); })();
const existingResources = db const existingResources = db