mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-01 08:16:44 +00:00
@@ -2349,6 +2349,7 @@
|
|||||||
"enterConfirmation": "Enter confirmation",
|
"enterConfirmation": "Enter confirmation",
|
||||||
"blueprintViewDetails": "Details",
|
"blueprintViewDetails": "Details",
|
||||||
"defaultIdentityProvider": "Default Identity Provider",
|
"defaultIdentityProvider": "Default Identity Provider",
|
||||||
|
"defaultIdentityProviderDescription": "When a default identity provider is selected, the user will be automatically redirected to the provider for authentication.",
|
||||||
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
||||||
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
||||||
"editInternalResourceDialogAddRoles": "Add Roles",
|
"editInternalResourceDialogAddRoles": "Add Roles",
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import {
|
|||||||
domains,
|
domains,
|
||||||
orgDomains,
|
orgDomains,
|
||||||
Resource,
|
Resource,
|
||||||
resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuth,
|
||||||
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
resourceRules,
|
resourceRules,
|
||||||
resourceWhitelist,
|
resourceWhitelist,
|
||||||
@@ -16,8 +17,8 @@ import {
|
|||||||
userResources,
|
userResources,
|
||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import {resources, targets, sites} from "@server/db";
|
import { resources, targets, sites } from "@server/db";
|
||||||
import {eq, and, asc, or, ne, count, isNotNull} from "drizzle-orm";
|
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
ConfigSchema,
|
ConfigSchema,
|
||||||
@@ -25,12 +26,13 @@ import {
|
|||||||
TargetData
|
TargetData
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {createCertificate} from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
import {pickPort} from "@server/routers/target/helpers";
|
import { pickPort } from "@server/routers/target/helpers";
|
||||||
import {resourcePassword} from "@server/db";
|
import { resourcePassword } from "@server/db";
|
||||||
import {hashPassword} from "@server/auth/password";
|
import { hashPassword } from "@server/auth/password";
|
||||||
import {isValidCIDR, isValidIP, isValidUrlGlobPattern} from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import {get} from "http";
|
import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export type ProxyResourcesResults = {
|
export type ProxyResourcesResults = {
|
||||||
proxyResource: Resource;
|
proxyResource: Resource;
|
||||||
@@ -63,7 +65,7 @@ export async function updateProxyResources(
|
|||||||
if (targetSiteId) {
|
if (targetSiteId) {
|
||||||
// Look up site by niceId
|
// Look up site by niceId
|
||||||
[site] = await trx
|
[site] = await trx
|
||||||
.select({siteId: sites.siteId})
|
.select({ siteId: sites.siteId })
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -75,7 +77,7 @@ export async function updateProxyResources(
|
|||||||
} else if (siteId) {
|
} else if (siteId) {
|
||||||
// Use the provided siteId directly, but verify it belongs to the org
|
// Use the provided siteId directly, but verify it belongs to the org
|
||||||
[site] = await trx
|
[site] = await trx
|
||||||
.select({siteId: sites.siteId})
|
.select({ siteId: sites.siteId })
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
||||||
@@ -93,7 +95,7 @@ export async function updateProxyResources(
|
|||||||
|
|
||||||
let internalPortToCreate;
|
let internalPortToCreate;
|
||||||
if (!targetData["internal-port"]) {
|
if (!targetData["internal-port"]) {
|
||||||
const {internalPort, targetIps} = await pickPort(
|
const { internalPort, targetIps } = await pickPort(
|
||||||
site.siteId!,
|
site.siteId!,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
@@ -209,6 +211,16 @@ export async function updateProxyResources(
|
|||||||
resource = existingResource;
|
resource = existingResource;
|
||||||
} else {
|
} else {
|
||||||
// Update existing resource
|
// Update existing resource
|
||||||
|
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
|
if (build == "enterprise" && !isLicensed) {
|
||||||
|
logger.warn(
|
||||||
|
"Server is not licensed! Clearing set maintenance screen values"
|
||||||
|
);
|
||||||
|
// null the maintenance mode fields if not licensed
|
||||||
|
resourceData.maintenance = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
[resource] = await trx
|
[resource] = await trx
|
||||||
.update(resources)
|
.update(resources)
|
||||||
.set({
|
.set({
|
||||||
@@ -228,12 +240,19 @@ export async function updateProxyResources(
|
|||||||
tlsServerName: resourceData["tls-server-name"] || null,
|
tlsServerName: resourceData["tls-server-name"] || null,
|
||||||
emailWhitelistEnabled: resourceData.auth?.[
|
emailWhitelistEnabled: resourceData.auth?.[
|
||||||
"whitelist-users"
|
"whitelist-users"
|
||||||
]
|
]
|
||||||
? resourceData.auth["whitelist-users"].length > 0
|
? resourceData.auth["whitelist-users"].length > 0
|
||||||
: false,
|
: false,
|
||||||
headers: headers || null,
|
headers: headers || null,
|
||||||
applyRules:
|
applyRules:
|
||||||
resourceData.rules && resourceData.rules.length > 0
|
resourceData.rules && resourceData.rules.length > 0,
|
||||||
|
maintenanceModeEnabled:
|
||||||
|
resourceData.maintenance?.enabled,
|
||||||
|
maintenanceModeType: resourceData.maintenance?.type,
|
||||||
|
maintenanceTitle: resourceData.maintenance?.title,
|
||||||
|
maintenanceMessage: resourceData.maintenance?.message,
|
||||||
|
maintenanceEstimatedTime:
|
||||||
|
resourceData.maintenance?.["estimated-time"]
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
eq(resources.resourceId, existingResource.resourceId)
|
eq(resources.resourceId, existingResource.resourceId)
|
||||||
@@ -303,8 +322,13 @@ export async function updateProxyResources(
|
|||||||
const headerAuthPassword =
|
const headerAuthPassword =
|
||||||
resourceData.auth?.["basic-auth"]?.password;
|
resourceData.auth?.["basic-auth"]?.password;
|
||||||
const headerAuthExtendedCompatibility =
|
const headerAuthExtendedCompatibility =
|
||||||
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
resourceData.auth?.["basic-auth"]
|
||||||
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
|
?.extendedCompatibility;
|
||||||
|
if (
|
||||||
|
headerAuthUser &&
|
||||||
|
headerAuthPassword &&
|
||||||
|
headerAuthExtendedCompatibility !== null
|
||||||
|
) {
|
||||||
const headerAuthHash = await hashPassword(
|
const headerAuthHash = await hashPassword(
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
`${headerAuthUser}:${headerAuthPassword}`
|
`${headerAuthUser}:${headerAuthPassword}`
|
||||||
@@ -315,10 +339,13 @@ export async function updateProxyResources(
|
|||||||
resourceId: existingResource.resourceId,
|
resourceId: existingResource.resourceId,
|
||||||
headerAuthHash
|
headerAuthHash
|
||||||
}),
|
}),
|
||||||
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
|
trx
|
||||||
resourceId: existingResource.resourceId,
|
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||||
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
|
.values({
|
||||||
})
|
resourceId: existingResource.resourceId,
|
||||||
|
extendedCompatibilityIsActivated:
|
||||||
|
headerAuthExtendedCompatibility
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +407,7 @@ export async function updateProxyResources(
|
|||||||
if (targetSiteId) {
|
if (targetSiteId) {
|
||||||
// Look up site by niceId
|
// Look up site by niceId
|
||||||
[site] = await trx
|
[site] = await trx
|
||||||
.select({siteId: sites.siteId})
|
.select({ siteId: sites.siteId })
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -392,7 +419,7 @@ export async function updateProxyResources(
|
|||||||
} else if (siteId) {
|
} else if (siteId) {
|
||||||
// Use the provided siteId directly, but verify it belongs to the org
|
// Use the provided siteId directly, but verify it belongs to the org
|
||||||
[site] = await trx
|
[site] = await trx
|
||||||
.select({siteId: sites.siteId})
|
.select({ siteId: sites.siteId })
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -437,7 +464,7 @@ export async function updateProxyResources(
|
|||||||
if (checkIfTargetChanged(existingTarget, updatedTarget)) {
|
if (checkIfTargetChanged(existingTarget, updatedTarget)) {
|
||||||
let internalPortToUpdate;
|
let internalPortToUpdate;
|
||||||
if (!targetData["internal-port"]) {
|
if (!targetData["internal-port"]) {
|
||||||
const {internalPort, targetIps} = await pickPort(
|
const { internalPort, targetIps } = await pickPort(
|
||||||
site.siteId!,
|
site.siteId!,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
@@ -622,6 +649,15 @@ export async function updateProxyResources(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
|
if (build == "enterprise" && !isLicensed) {
|
||||||
|
logger.warn(
|
||||||
|
"Server is not licensed! Clearing set maintenance screen values"
|
||||||
|
);
|
||||||
|
// null the maintenance mode fields if not licensed
|
||||||
|
resourceData.maintenance = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Create new resource
|
// Create new resource
|
||||||
const [newResource] = await trx
|
const [newResource] = await trx
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
@@ -643,7 +679,13 @@ export async function updateProxyResources(
|
|||||||
ssl: resourceSsl,
|
ssl: resourceSsl,
|
||||||
headers: headers || null,
|
headers: headers || null,
|
||||||
applyRules:
|
applyRules:
|
||||||
resourceData.rules && resourceData.rules.length > 0
|
resourceData.rules && resourceData.rules.length > 0,
|
||||||
|
maintenanceModeEnabled: resourceData.maintenance?.enabled,
|
||||||
|
maintenanceModeType: resourceData.maintenance?.type,
|
||||||
|
maintenanceTitle: resourceData.maintenance?.title,
|
||||||
|
maintenanceMessage: resourceData.maintenance?.message,
|
||||||
|
maintenanceEstimatedTime:
|
||||||
|
resourceData.maintenance?.["estimated-time"]
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -674,9 +716,14 @@ export async function updateProxyResources(
|
|||||||
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
|
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
|
||||||
const headerAuthPassword =
|
const headerAuthPassword =
|
||||||
resourceData.auth?.["basic-auth"]?.password;
|
resourceData.auth?.["basic-auth"]?.password;
|
||||||
const headerAuthExtendedCompatibility = resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
const headerAuthExtendedCompatibility =
|
||||||
|
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
||||||
|
|
||||||
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
|
if (
|
||||||
|
headerAuthUser &&
|
||||||
|
headerAuthPassword &&
|
||||||
|
headerAuthExtendedCompatibility !== null
|
||||||
|
) {
|
||||||
const headerAuthHash = await hashPassword(
|
const headerAuthHash = await hashPassword(
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
`${headerAuthUser}:${headerAuthPassword}`
|
`${headerAuthUser}:${headerAuthPassword}`
|
||||||
@@ -688,10 +735,13 @@ export async function updateProxyResources(
|
|||||||
resourceId: newResource.resourceId,
|
resourceId: newResource.resourceId,
|
||||||
headerAuthHash
|
headerAuthHash
|
||||||
}),
|
}),
|
||||||
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
|
trx
|
||||||
resourceId: newResource.resourceId,
|
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||||
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
|
.values({
|
||||||
}),
|
resourceId: newResource.resourceId,
|
||||||
|
extendedCompatibilityIsActivated:
|
||||||
|
headerAuthExtendedCompatibility
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1043,7 +1093,7 @@ async function getDomain(
|
|||||||
trx: Transaction
|
trx: Transaction
|
||||||
) {
|
) {
|
||||||
const [fullDomainExists] = await trx
|
const [fullDomainExists] = await trx
|
||||||
.select({resourceId: resources.resourceId})
|
.select({ resourceId: resources.resourceId })
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { portRangeStringSchema } from "@server/lib/ip";
|
import { portRangeStringSchema } from "@server/lib/ip";
|
||||||
|
import { MaintenanceSchema } from "#dynamic/lib/blueprints/types";
|
||||||
|
|
||||||
export const SiteSchema = z.object({
|
export const SiteSchema = z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
@@ -156,7 +157,8 @@ export const ResourceSchema = z
|
|||||||
"host-header": z.string().optional(),
|
"host-header": z.string().optional(),
|
||||||
"tls-server-name": z.string().optional(),
|
"tls-server-name": z.string().optional(),
|
||||||
headers: z.array(HeaderSchema).optional(),
|
headers: z.array(HeaderSchema).optional(),
|
||||||
rules: z.array(RuleSchema).optional()
|
rules: z.array(RuleSchema).optional(),
|
||||||
|
maintenance: MaintenanceSchema.optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(resource) => {
|
(resource) => {
|
||||||
|
|||||||
9
server/private/lib/blueprints/types.ts
Normal file
9
server/private/lib/blueprints/types.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const MaintenanceSchema = z.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
type: z.enum(["forced", "automatic"]).optional(),
|
||||||
|
title: z.string().max(255).nullable().optional(),
|
||||||
|
message: z.string().max(2000).nullable().optional(),
|
||||||
|
"estimated-time": z.string().max(100).nullable().optional()
|
||||||
|
});
|
||||||
@@ -105,6 +105,7 @@ async function query(query: Q) {
|
|||||||
// throw an error
|
// throw an error
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
|
// todo: is this even possible?
|
||||||
`Too many distinct countries. Please narrow your query.`
|
`Too many distinct countries. Please narrow your query.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,15 +219,17 @@ async function queryUniqueFilterAttributes(
|
|||||||
.limit(DISTINCT_LIMIT+1)
|
.limit(DISTINCT_LIMIT+1)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
// TODO: for stuff like the paths this is too restrictive so lets just show some of the paths and the user needs to
|
||||||
uniqueActors.length > DISTINCT_LIMIT ||
|
// refine the time range to see what they need to see
|
||||||
uniqueLocations.length > DISTINCT_LIMIT ||
|
// if (
|
||||||
uniqueHosts.length > DISTINCT_LIMIT ||
|
// uniqueActors.length > DISTINCT_LIMIT ||
|
||||||
uniquePaths.length > DISTINCT_LIMIT ||
|
// uniqueLocations.length > DISTINCT_LIMIT ||
|
||||||
uniqueResources.length > DISTINCT_LIMIT
|
// uniqueHosts.length > DISTINCT_LIMIT ||
|
||||||
) {
|
// uniquePaths.length > DISTINCT_LIMIT ||
|
||||||
throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
|
// uniqueResources.length > DISTINCT_LIMIT
|
||||||
}
|
// ) {
|
||||||
|
// throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
|
||||||
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actors: uniqueActors
|
actors: uniqueActors
|
||||||
|
|||||||
@@ -62,7 +62,26 @@ export async function exchangeSession(
|
|||||||
cleanHost = cleanHost.slice(0, -1 * matched.length);
|
cleanHost = cleanHost.slice(0, -1 * matched.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientIp = requestIp?.split(":")[0];
|
const clientIp = requestIp
|
||||||
|
? (() => {
|
||||||
|
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
||||||
|
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
||||||
|
if (ipv6Match) {
|
||||||
|
return ipv6Match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}/;
|
||||||
|
if (ipv4Pattern.test(requestIp)) {
|
||||||
|
const lastColonIndex = requestIp.lastIndexOf(":");
|
||||||
|
if (lastColonIndex !== -1) {
|
||||||
|
return requestIp.substring(0, lastColonIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestIp;
|
||||||
|
})()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const [resource] = await db
|
const [resource] = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
LoginPage,
|
LoginPage,
|
||||||
Org,
|
Org,
|
||||||
Resource,
|
Resource,
|
||||||
ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuth,
|
||||||
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
ResourcePincode,
|
ResourcePincode,
|
||||||
ResourceRule,
|
ResourceRule,
|
||||||
@@ -39,6 +40,8 @@ import {
|
|||||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { logRequestAudit } from "./logRequestAudit";
|
import { logRequestAudit } from "./logRequestAudit";
|
||||||
import cache from "@server/lib/cache";
|
import cache from "@server/lib/cache";
|
||||||
|
import semver from "semver";
|
||||||
|
import { APP_VERSION } from "@server/lib/consts";
|
||||||
|
|
||||||
const verifyResourceSessionSchema = z.object({
|
const verifyResourceSessionSchema = z.object({
|
||||||
sessions: z.record(z.string(), z.string()).optional(),
|
sessions: z.record(z.string(), z.string()).optional(),
|
||||||
@@ -50,7 +53,8 @@ const verifyResourceSessionSchema = z.object({
|
|||||||
path: z.string(),
|
path: z.string(),
|
||||||
method: z.string(),
|
method: z.string(),
|
||||||
tls: z.boolean(),
|
tls: z.boolean(),
|
||||||
requestIp: z.string().optional()
|
requestIp: z.string().optional(),
|
||||||
|
badgerVersion: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type VerifyResourceSessionSchema = z.infer<
|
export type VerifyResourceSessionSchema = z.infer<
|
||||||
@@ -69,6 +73,7 @@ export type VerifyUserResponse = {
|
|||||||
headerAuthChallenged?: boolean;
|
headerAuthChallenged?: boolean;
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
userData?: BasicUserData;
|
userData?: BasicUserData;
|
||||||
|
pangolinVersion?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function verifyResourceSession(
|
export async function verifyResourceSession(
|
||||||
@@ -97,7 +102,8 @@ export async function verifyResourceSession(
|
|||||||
requestIp,
|
requestIp,
|
||||||
path,
|
path,
|
||||||
headers,
|
headers,
|
||||||
query
|
query,
|
||||||
|
badgerVersion
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
// Extract HTTP Basic Auth credentials if present
|
// Extract HTTP Basic Auth credentials if present
|
||||||
@@ -105,7 +111,15 @@ export async function verifyResourceSession(
|
|||||||
|
|
||||||
const clientIp = requestIp
|
const clientIp = requestIp
|
||||||
? (() => {
|
? (() => {
|
||||||
logger.debug("Request IP:", { requestIp });
|
const isNewerBadger =
|
||||||
|
badgerVersion &&
|
||||||
|
semver.valid(badgerVersion) &&
|
||||||
|
semver.gte(badgerVersion, "1.3.1");
|
||||||
|
|
||||||
|
if (isNewerBadger) {
|
||||||
|
return requestIp;
|
||||||
|
}
|
||||||
|
|
||||||
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
||||||
// if brackets are found, extract the IPv6 address from between the brackets
|
// if brackets are found, extract the IPv6 address from between the brackets
|
||||||
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
||||||
@@ -114,12 +128,17 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ivp4
|
// Check if it looks like IPv4 (contains dots and matches IPv4 pattern)
|
||||||
// split at last colon
|
// IPv4 format: x.x.x.x where x is 0-255
|
||||||
const lastColonIndex = requestIp.lastIndexOf(":");
|
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}/;
|
||||||
if (lastColonIndex !== -1) {
|
if (ipv4Pattern.test(requestIp)) {
|
||||||
return requestIp.substring(0, lastColonIndex);
|
const lastColonIndex = requestIp.lastIndexOf(":");
|
||||||
|
if (lastColonIndex !== -1) {
|
||||||
|
return requestIp.substring(0, lastColonIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return as is
|
||||||
return requestIp;
|
return requestIp;
|
||||||
})()
|
})()
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -130,9 +149,7 @@ export async function verifyResourceSession(
|
|||||||
? await getCountryCodeFromIp(clientIp)
|
? await getCountryCodeFromIp(clientIp)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const ipAsn = clientIp
|
const ipAsn = clientIp ? await getAsnFromIp(clientIp) : undefined;
|
||||||
? await getAsnFromIp(clientIp)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let cleanHost = host;
|
let cleanHost = host;
|
||||||
// if the host ends with :port, strip it
|
// if the host ends with :port, strip it
|
||||||
@@ -178,7 +195,13 @@ export async function verifyResourceSession(
|
|||||||
cache.set(resourceCacheKey, resourceData, 5);
|
cache.set(resourceCacheKey, resourceData, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData;
|
const {
|
||||||
|
resource,
|
||||||
|
pincode,
|
||||||
|
password,
|
||||||
|
headerAuth,
|
||||||
|
headerAuthExtendedCompatibility
|
||||||
|
} = resourceData;
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
logger.debug(`Resource not found ${cleanHost}`);
|
logger.debug(`Resource not found ${cleanHost}`);
|
||||||
@@ -474,8 +497,7 @@ export async function verifyResourceSession(
|
|||||||
|
|
||||||
return notAllowed(res);
|
return notAllowed(res);
|
||||||
}
|
}
|
||||||
}
|
} else if (headerAuth) {
|
||||||
else if (headerAuth) {
|
|
||||||
// if there are no other auth methods we need to return unauthorized if nothing is provided
|
// if there are no other auth methods we need to return unauthorized if nothing is provided
|
||||||
if (
|
if (
|
||||||
!sso &&
|
!sso &&
|
||||||
@@ -713,7 +735,11 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge
|
// If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge
|
||||||
if (headerAuthExtendedCompatibility && headerAuthExtendedCompatibility.extendedCompatibilityIsActivated && !clientHeaderAuth){
|
if (
|
||||||
|
headerAuthExtendedCompatibility &&
|
||||||
|
headerAuthExtendedCompatibility.extendedCompatibilityIsActivated &&
|
||||||
|
!clientHeaderAuth
|
||||||
|
) {
|
||||||
return headerAuthChallenged(res, redirectPath, resource.orgId);
|
return headerAuthChallenged(res, redirectPath, resource.orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +851,7 @@ async function notAllowed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
data: { valid: false, redirectUrl },
|
data: { valid: false, redirectUrl, pangolinVersion: APP_VERSION },
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access denied",
|
message: "Access denied",
|
||||||
@@ -839,8 +865,8 @@ function allowed(res: Response, userData?: BasicUserData) {
|
|||||||
const data = {
|
const data = {
|
||||||
data:
|
data:
|
||||||
userData !== undefined && userData !== null
|
userData !== undefined && userData !== null
|
||||||
? { valid: true, ...userData }
|
? { valid: true, ...userData, pangolinVersion: APP_VERSION }
|
||||||
: { valid: true },
|
: { valid: true, pangolinVersion: APP_VERSION },
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access allowed",
|
message: "Access allowed",
|
||||||
@@ -879,7 +905,12 @@ async function headerAuthChallenged(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
data: { headerAuthChallenged: true, valid: false, redirectUrl },
|
data: {
|
||||||
|
headerAuthChallenged: true,
|
||||||
|
valid: false,
|
||||||
|
redirectUrl,
|
||||||
|
pangolinVersion: APP_VERSION
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access denied",
|
message: "Access denied",
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ async function getLatestOlmVersion(): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await response.json();
|
let tags = await response.json();
|
||||||
if (!Array.isArray(tags) || tags.length === 0) {
|
if (!Array.isArray(tags) || tags.length === 0) {
|
||||||
logger.warn("No tags found for Olm repository");
|
logger.warn("No tags found for Olm repository");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||||
const latestVersion = tags[0].name;
|
const latestVersion = tags[0].name;
|
||||||
|
|
||||||
olmVersionCache.set("latestOlmVersion", latestVersion);
|
olmVersionCache.set("latestOlmVersion", latestVersion);
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await response.json();
|
let tags = await response.json();
|
||||||
if (!Array.isArray(tags) || tags.length === 0) {
|
if (!Array.isArray(tags) || tags.length === 0) {
|
||||||
logger.warn("No tags found for Newt repository");
|
logger.warn("No tags found for Newt repository");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||||
const latestVersion = tags[0].name;
|
const latestVersion = tags[0].name;
|
||||||
|
|
||||||
cache.set("latestNewtVersion", latestVersion);
|
cache.set("latestNewtVersion", latestVersion);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { SwitchInput } from "@app/components/SwitchInput";
|
|||||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { CheckboxWithLabel } from "@app/components/ui/checkbox";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -184,9 +183,6 @@ export default function ResourceAuthenticationPage() {
|
|||||||
|
|
||||||
const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
|
const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
|
||||||
|
|
||||||
const [autoLoginEnabled, setAutoLoginEnabled] = useState(
|
|
||||||
resource.skipToIdpId !== null && resource.skipToIdpId !== undefined
|
|
||||||
);
|
|
||||||
const [selectedIdpId, setSelectedIdpId] = useState<number | null>(
|
const [selectedIdpId, setSelectedIdpId] = useState<number | null>(
|
||||||
resource.skipToIdpId || null
|
resource.skipToIdpId || null
|
||||||
);
|
);
|
||||||
@@ -243,17 +239,12 @@ export default function ResourceAuthenticationPage() {
|
|||||||
text: w.email
|
text: w.email
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
if (autoLoginEnabled && !selectedIdpId && orgIdps.length > 0) {
|
|
||||||
setSelectedIdpId(orgIdps[0].idpId);
|
|
||||||
}
|
|
||||||
hasInitializedRef.current = true;
|
hasInitializedRef.current = true;
|
||||||
}, [
|
}, [
|
||||||
pageLoading,
|
pageLoading,
|
||||||
resourceRoles,
|
resourceRoles,
|
||||||
resourceUsers,
|
resourceUsers,
|
||||||
whitelist,
|
whitelist,
|
||||||
autoLoginEnabled,
|
|
||||||
selectedIdpId,
|
|
||||||
orgIdps
|
orgIdps
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -269,16 +260,6 @@ export default function ResourceAuthenticationPage() {
|
|||||||
const data = usersRolesForm.getValues();
|
const data = usersRolesForm.getValues();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate that an IDP is selected if auto login is enabled
|
|
||||||
if (autoLoginEnabled && !selectedIdpId) {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("error"),
|
|
||||||
description: t("selectIdpRequired")
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobs = [
|
const jobs = [
|
||||||
api.post(`/resource/${resource.resourceId}/roles`, {
|
api.post(`/resource/${resource.resourceId}/roles`, {
|
||||||
roleIds: data.roles.map((i) => parseInt(i.id))
|
roleIds: data.roles.map((i) => parseInt(i.id))
|
||||||
@@ -288,7 +269,7 @@ export default function ResourceAuthenticationPage() {
|
|||||||
}),
|
}),
|
||||||
api.post(`/resource/${resource.resourceId}`, {
|
api.post(`/resource/${resource.resourceId}`, {
|
||||||
sso: ssoEnabled,
|
sso: ssoEnabled,
|
||||||
skipToIdpId: autoLoginEnabled ? selectedIdpId : null
|
skipToIdpId: selectedIdpId
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -296,7 +277,7 @@ export default function ResourceAuthenticationPage() {
|
|||||||
|
|
||||||
updateResource({
|
updateResource({
|
||||||
sso: ssoEnabled,
|
sso: ssoEnabled,
|
||||||
skipToIdpId: autoLoginEnabled ? selectedIdpId : null
|
skipToIdpId: selectedIdpId
|
||||||
});
|
});
|
||||||
|
|
||||||
updateAuthInfo({
|
updateAuthInfo({
|
||||||
@@ -619,88 +600,55 @@ export default function ResourceAuthenticationPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{ssoEnabled && allIdps.length > 0 && (
|
{ssoEnabled && allIdps.length > 0 && (
|
||||||
<>
|
<div className="space-y-2">
|
||||||
<div className="space-y-2 mb-3">
|
<label className="text-sm font-medium">
|
||||||
<CheckboxWithLabel
|
{t(
|
||||||
label={t(
|
"defaultIdentityProvider"
|
||||||
"autoLoginExternalIdp"
|
)}
|
||||||
)}
|
</label>
|
||||||
checked={autoLoginEnabled}
|
<Select
|
||||||
onCheckedChange={(
|
onValueChange={(value) => {
|
||||||
checked
|
if (value === "none") {
|
||||||
) => {
|
setSelectedIdpId(null);
|
||||||
setAutoLoginEnabled(
|
} else {
|
||||||
checked as boolean
|
setSelectedIdpId(
|
||||||
|
parseInt(value)
|
||||||
);
|
);
|
||||||
if (
|
}
|
||||||
checked &&
|
}}
|
||||||
allIdps.length > 0
|
value={
|
||||||
) {
|
selectedIdpId
|
||||||
setSelectedIdpId(
|
? selectedIdpId.toString()
|
||||||
allIdps[0].id
|
: "none"
|
||||||
);
|
}
|
||||||
} else {
|
>
|
||||||
setSelectedIdpId(
|
<SelectTrigger className="w-full mt-1">
|
||||||
null
|
<SelectValue
|
||||||
);
|
placeholder={t(
|
||||||
}
|
"selectIdpPlaceholder"
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"autoLoginExternalIdpDescription"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{autoLoginEnabled && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">
|
|
||||||
{t(
|
|
||||||
"defaultIdentityProvider"
|
|
||||||
)}
|
)}
|
||||||
</label>
|
/>
|
||||||
<Select
|
</SelectTrigger>
|
||||||
onValueChange={(
|
<SelectContent>
|
||||||
value
|
<SelectItem value="none">
|
||||||
) =>
|
{t("none")}
|
||||||
setSelectedIdpId(
|
</SelectItem>
|
||||||
parseInt(value)
|
{allIdps.map((idp) => (
|
||||||
)
|
<SelectItem
|
||||||
}
|
key={idp.id}
|
||||||
value={
|
value={idp.id.toString()}
|
||||||
selectedIdpId
|
>
|
||||||
? selectedIdpId.toString()
|
{idp.text}
|
||||||
: undefined
|
</SelectItem>
|
||||||
}
|
))}
|
||||||
>
|
</SelectContent>
|
||||||
<SelectTrigger className="w-full mt-1">
|
</Select>
|
||||||
<SelectValue
|
<p className="text-sm text-muted-foreground">
|
||||||
placeholder={t(
|
{t(
|
||||||
"selectIdpPlaceholder"
|
"defaultIdentityProviderDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
</p>
|
||||||
</SelectTrigger>
|
</div>
|
||||||
<SelectContent>
|
|
||||||
{allIdps.map(
|
|
||||||
(idp) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
idp.id
|
|
||||||
}
|
|
||||||
value={idp.id.toString()}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
idp.text
|
|
||||||
}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -87,8 +87,6 @@ export default async function OrgAuthPage(props: {
|
|||||||
redirect(env.app.dashboardUrl);
|
redirect(env.app.dashboardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(user, forceLogin);
|
|
||||||
|
|
||||||
if (user && !forceLogin) {
|
if (user && !forceLogin) {
|
||||||
let redirectToken: string | undefined;
|
let redirectToken: string | undefined;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function OrgSelectionForm() {
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("orgAuthWhatsThis")}{" "}
|
{t("orgAuthWhatsThis")}{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.pangolin.net/manage/identity-providers/add-an-idp"
|
href="https://docs.pangolin.net/manage/organizations/org-id"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline"
|
className="underline"
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ export default function ProductUpdates({
|
|||||||
|
|
||||||
const data = useQueries({
|
const data = useQueries({
|
||||||
queries: [
|
queries: [
|
||||||
productUpdatesQueries.list(env.app.notifications.product_updates),
|
productUpdatesQueries.list(
|
||||||
|
env.app.notifications.product_updates,
|
||||||
|
env.app.version
|
||||||
|
),
|
||||||
productUpdatesQueries.latestVersion(
|
productUpdatesQueries.latestVersion(
|
||||||
env.app.notifications.new_releases
|
env.app.notifications.new_releases
|
||||||
)
|
)
|
||||||
@@ -78,10 +81,10 @@ export default function ProductUpdates({
|
|||||||
|
|
||||||
const showNewVersionPopup = Boolean(
|
const showNewVersionPopup = Boolean(
|
||||||
latestVersion &&
|
latestVersion &&
|
||||||
valid(latestVersion) &&
|
valid(latestVersion) &&
|
||||||
valid(currentVersion) &&
|
valid(currentVersion) &&
|
||||||
ignoredVersionUpdate !== latestVersion &&
|
ignoredVersionUpdate !== latestVersion &&
|
||||||
gt(latestVersion, currentVersion)
|
gt(latestVersion, currentVersion)
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredUpdates = data.updates.filter(
|
const filteredUpdates = data.updates.filter(
|
||||||
|
|||||||
@@ -41,12 +41,13 @@ export type LatestVersionResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const productUpdatesQueries = {
|
export const productUpdatesQueries = {
|
||||||
list: (enabled: boolean) =>
|
list: (enabled: boolean, version?: string) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["PRODUCT_UPDATES"] as const,
|
queryKey: ["PRODUCT_UPDATES"] as const,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
const sp = new URLSearchParams({
|
const sp = new URLSearchParams({
|
||||||
build
|
build,
|
||||||
|
...(version ? { version } : {})
|
||||||
});
|
});
|
||||||
const data = await remote.get<ResponseT<ProductUpdate[]>>(
|
const data = await remote.get<ResponseT<ProductUpdate[]>>(
|
||||||
`/product-updates?${sp.toString()}`,
|
`/product-updates?${sp.toString()}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user