mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-17 09:49:55 +02:00
Support pin,pass,whitelist correctly on login
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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" })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user