mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-11 23:46:50 +00:00
Merge branch 'dev' into auth-providers-clients
This commit is contained in:
@@ -29,9 +29,12 @@ const configSchema = z.object({
|
||||
.optional()
|
||||
.pipe(z.string().url())
|
||||
.transform((url) => url.toLowerCase()),
|
||||
log_level: z.enum(["debug", "info", "warn", "error"]),
|
||||
save_logs: z.boolean(),
|
||||
log_failed_attempts: z.boolean().optional()
|
||||
log_level: z
|
||||
.enum(["debug", "info", "warn", "error"])
|
||||
.optional()
|
||||
.default("info"),
|
||||
save_logs: z.boolean().optional().default(false),
|
||||
log_failed_attempts: z.boolean().optional().default(false)
|
||||
}),
|
||||
domains: z
|
||||
.record(
|
||||
@@ -41,8 +44,8 @@ const configSchema = z.object({
|
||||
.string()
|
||||
.nonempty("base_domain must not be empty")
|
||||
.transform((url) => url.toLowerCase()),
|
||||
cert_resolver: z.string().optional(),
|
||||
prefer_wildcard_cert: z.boolean().optional()
|
||||
cert_resolver: z.string().optional().default("letsencrypt"),
|
||||
prefer_wildcard_cert: z.boolean().optional().default(false)
|
||||
})
|
||||
)
|
||||
.refine(
|
||||
@@ -62,19 +65,42 @@ const configSchema = z.object({
|
||||
server: z.object({
|
||||
integration_port: portSchema
|
||||
.optional()
|
||||
.default(3003)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema.optional()),
|
||||
external_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||
internal_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||
next_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||
session_cookie_name: z.string(),
|
||||
resource_access_token_param: z.string(),
|
||||
resource_access_token_headers: z.object({
|
||||
id: z.string(),
|
||||
token: z.string()
|
||||
}),
|
||||
resource_session_request_param: z.string(),
|
||||
external_port: portSchema
|
||||
.optional()
|
||||
.default(3000)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_port: portSchema
|
||||
.optional()
|
||||
.default(3001)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
next_port: portSchema
|
||||
.optional()
|
||||
.default(3002)
|
||||
.transform(stoi)
|
||||
.pipe(portSchema),
|
||||
internal_hostname: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("pangolin")
|
||||
.transform((url) => url.toLowerCase()),
|
||||
session_cookie_name: z.string().optional().default("p_session_token"),
|
||||
resource_access_token_param: z.string().optional().default("p_token"),
|
||||
resource_access_token_headers: z
|
||||
.object({
|
||||
id: z.string().optional().default("P-Access-Token-Id"),
|
||||
token: z.string().optional().default("P-Access-Token")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
resource_session_request_param: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("resource_session_request_param"),
|
||||
dashboard_session_length_hours: z
|
||||
.number()
|
||||
.positive()
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.3.0";
|
||||
export const APP_VERSION = "1.3.2";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
@@ -22,7 +22,7 @@ function detectIpVersion(ip: string): IPVersion {
|
||||
*/
|
||||
function ipToBigInt(ip: string): bigint {
|
||||
const version = detectIpVersion(ip);
|
||||
|
||||
|
||||
if (version === 4) {
|
||||
return ip.split('.')
|
||||
.reduce((acc, octet) => {
|
||||
@@ -110,7 +110,7 @@ export function cidrToRange(cidr: string): IPRange {
|
||||
const version = detectIpVersion(ip);
|
||||
const prefixBits = parseInt(prefix);
|
||||
const ipBigInt = ipToBigInt(ip);
|
||||
|
||||
|
||||
// Validate prefix length
|
||||
const maxPrefix = version === 4 ? 32 : 128;
|
||||
if (prefixBits < 0 || prefixBits > maxPrefix) {
|
||||
@@ -121,7 +121,7 @@ export function cidrToRange(cidr: string): IPRange {
|
||||
const mask = BigInt.asUintN(version === 4 ? 64 : 128, (BigInt(1) << shiftBits) - BigInt(1));
|
||||
const start = ipBigInt & ~mask;
|
||||
const end = start | mask;
|
||||
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
@@ -140,17 +140,17 @@ export function findNextAvailableCidr(
|
||||
if (!startCidr && existingCidrs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// If no existing CIDRs, use the IP version from startCidr
|
||||
const version = startCidr
|
||||
const version = startCidr
|
||||
? detectIpVersion(startCidr.split('/')[0])
|
||||
: 4; // Default to IPv4 if no startCidr provided
|
||||
|
||||
|
||||
// Use appropriate default startCidr if none provided
|
||||
startCidr = startCidr || (version === 4 ? "0.0.0.0/0" : "::/0");
|
||||
|
||||
|
||||
// If there are existing CIDRs, ensure all are same version
|
||||
if (existingCidrs.length > 0 &&
|
||||
if (existingCidrs.length > 0 &&
|
||||
existingCidrs.some(cidr => detectIpVersion(cidr.split('/')[0]) !== version)) {
|
||||
throw new Error('All CIDRs must be of the same IP version');
|
||||
}
|
||||
@@ -207,9 +207,11 @@ export function findNextAvailableCidr(
|
||||
export function isIpInCidr(ip: string, cidr: string): boolean {
|
||||
const ipVersion = detectIpVersion(ip);
|
||||
const cidrVersion = detectIpVersion(cidr.split('/')[0]);
|
||||
|
||||
|
||||
// If IP versions don't match, the IP cannot be in the CIDR range
|
||||
if (ipVersion !== cidrVersion) {
|
||||
throw new Error('IP address and CIDR must be of the same version');
|
||||
// throw new Erorr
|
||||
return false;
|
||||
}
|
||||
|
||||
const ipBigInt = ipToBigInt(ip);
|
||||
@@ -274,4 +276,4 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
|
||||
}
|
||||
|
||||
return subnet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ export function isValidIP(ip: string): boolean {
|
||||
}
|
||||
|
||||
export function isValidUrlGlobPattern(pattern: string): boolean {
|
||||
if (pattern === "/") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove leading slash if present
|
||||
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
||||
|
||||
|
||||
@@ -1,61 +1,136 @@
|
||||
import { isPathAllowed } from './verifySession';
|
||||
import { assertEquals } from '@test/assert';
|
||||
|
||||
function isPathAllowed(pattern: string, path: string): boolean {
|
||||
|
||||
// Normalize and split paths into segments
|
||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
||||
const patternParts = normalize(pattern);
|
||||
const pathParts = normalize(path);
|
||||
|
||||
|
||||
// Recursive function to try different wildcard matches
|
||||
function matchSegments(patternIndex: number, pathIndex: number): boolean {
|
||||
const indent = " ".repeat(pathIndex); // Indent based on recursion depth
|
||||
const currentPatternPart = patternParts[patternIndex];
|
||||
const currentPathPart = pathParts[pathIndex];
|
||||
|
||||
// If we've consumed all pattern parts, we should have consumed all path parts
|
||||
if (patternIndex >= patternParts.length) {
|
||||
const result = pathIndex >= pathParts.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we've consumed all path parts but still have pattern parts
|
||||
if (pathIndex >= pathParts.length) {
|
||||
// The only way this can match is if all remaining pattern parts are wildcards
|
||||
const remainingPattern = patternParts.slice(patternIndex);
|
||||
const result = remainingPattern.every((p) => p === "*");
|
||||
return result;
|
||||
}
|
||||
|
||||
// For full segment wildcards, try consuming different numbers of path segments
|
||||
if (currentPatternPart === "*") {
|
||||
|
||||
// Try consuming 0 segments (skip the wildcard)
|
||||
if (matchSegments(patternIndex + 1, pathIndex)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try consuming current segment and recursively try rest
|
||||
if (matchSegments(patternIndex, pathIndex + 1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix")
|
||||
if (currentPatternPart.includes("*")) {
|
||||
// Convert the pattern segment to a regex pattern
|
||||
const regexPattern = currentPatternPart
|
||||
.replace(/\*/g, ".*") // Replace * with .* for regex wildcard
|
||||
.replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed
|
||||
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
|
||||
if (regex.test(currentPathPart)) {
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// For regular segments, they must match exactly
|
||||
if (currentPatternPart !== currentPathPart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Move to next segments in both pattern and path
|
||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
||||
}
|
||||
|
||||
const result = matchSegments(0, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
function runTests() {
|
||||
console.log('Running path matching tests...');
|
||||
|
||||
|
||||
// Test exact matching
|
||||
assertEquals(isPathAllowed('foo', 'foo'), true, 'Exact match should be allowed');
|
||||
assertEquals(isPathAllowed('foo', 'bar'), false, 'Different segments should not match');
|
||||
assertEquals(isPathAllowed('foo/bar', 'foo/bar'), true, 'Exact multi-segment match should be allowed');
|
||||
assertEquals(isPathAllowed('foo/bar', 'foo/baz'), false, 'Partial multi-segment match should not be allowed');
|
||||
|
||||
|
||||
// Test with leading and trailing slashes
|
||||
assertEquals(isPathAllowed('/foo', 'foo'), true, 'Pattern with leading slash should match');
|
||||
assertEquals(isPathAllowed('foo/', 'foo'), true, 'Pattern with trailing slash should match');
|
||||
assertEquals(isPathAllowed('/foo/', 'foo'), true, 'Pattern with both leading and trailing slashes should match');
|
||||
assertEquals(isPathAllowed('foo', '/foo/'), true, 'Path with leading and trailing slashes should match');
|
||||
|
||||
|
||||
// Test simple wildcard matching
|
||||
assertEquals(isPathAllowed('*', 'foo'), true, 'Single wildcard should match any single segment');
|
||||
assertEquals(isPathAllowed('*', 'foo/bar'), true, 'Single wildcard should match multiple segments');
|
||||
assertEquals(isPathAllowed('*/bar', 'foo/bar'), true, 'Wildcard prefix should match');
|
||||
assertEquals(isPathAllowed('foo/*', 'foo/bar'), true, 'Wildcard suffix should match');
|
||||
assertEquals(isPathAllowed('foo/*/baz', 'foo/bar/baz'), true, 'Wildcard in middle should match');
|
||||
|
||||
|
||||
// Test multiple wildcards
|
||||
assertEquals(isPathAllowed('*/*', 'foo/bar'), true, 'Multiple wildcards should match corresponding segments');
|
||||
assertEquals(isPathAllowed('*/*/*', 'foo/bar/baz'), true, 'Three wildcards should match three segments');
|
||||
assertEquals(isPathAllowed('foo/*/*', 'foo/bar/baz'), true, 'Specific prefix with wildcards should match');
|
||||
assertEquals(isPathAllowed('*/*/baz', 'foo/bar/baz'), true, 'Wildcards with specific suffix should match');
|
||||
|
||||
|
||||
// Test wildcard consumption behavior
|
||||
assertEquals(isPathAllowed('*', ''), true, 'Wildcard should optionally consume segments');
|
||||
assertEquals(isPathAllowed('foo/*', 'foo'), true, 'Trailing wildcard should be optional');
|
||||
assertEquals(isPathAllowed('*/*', 'foo'), true, 'Multiple wildcards can match fewer segments');
|
||||
assertEquals(isPathAllowed('*/*/*', 'foo/bar'), true, 'Extra wildcards can be skipped');
|
||||
|
||||
|
||||
// Test complex nested paths
|
||||
assertEquals(isPathAllowed('api/*/users', 'api/v1/users'), true, 'API versioning pattern should match');
|
||||
assertEquals(isPathAllowed('api/*/users/*', 'api/v1/users/123'), true, 'API resource pattern should match');
|
||||
assertEquals(isPathAllowed('api/*/users/*/profile', 'api/v1/users/123/profile'), true, 'Nested API pattern should match');
|
||||
|
||||
|
||||
// Test for the requested padbootstrap* pattern
|
||||
assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap'), true, 'padbootstrap* should match padbootstrap');
|
||||
assertEquals(isPathAllowed('padbootstrap*', 'padbootstrapv1'), true, 'padbootstrap* should match padbootstrapv1');
|
||||
assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap/files'), false, 'padbootstrap* should not match padbootstrap/files');
|
||||
assertEquals(isPathAllowed('padbootstrap*/*', 'padbootstrap/files'), true, 'padbootstrap*/* should match padbootstrap/files');
|
||||
assertEquals(isPathAllowed('padbootstrap*/files', 'padbootstrapv1/files'), true, 'padbootstrap*/files should not match padbootstrapv1/files (wildcard is segment-based, not partial)');
|
||||
|
||||
|
||||
// Test wildcard edge cases
|
||||
assertEquals(isPathAllowed('*/*/*/*/*/*', 'a/b'), true, 'Many wildcards can match few segments');
|
||||
assertEquals(isPathAllowed('a/*/b/*/c', 'a/anything/b/something/c'), true, 'Multiple wildcards in pattern should match corresponding segments');
|
||||
|
||||
|
||||
// Test patterns with partial segment matches
|
||||
assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap-123'), true, 'Wildcards in isPathAllowed should be segment-based, not character-based');
|
||||
assertEquals(isPathAllowed('test*', 'testuser'), true, 'Asterisk as part of segment name is treated as a literal, not a wildcard');
|
||||
assertEquals(isPathAllowed('my*app', 'myapp'), true, 'Asterisk in middle of segment name is treated as a literal, not a wildcard');
|
||||
|
||||
assertEquals(isPathAllowed('/', '/'), true, 'Root path should match root path');
|
||||
assertEquals(isPathAllowed('/', '/test'), false, 'Root path should not match non-root path');
|
||||
|
||||
console.log('All tests passed!');
|
||||
}
|
||||
|
||||
@@ -64,4 +139,4 @@ try {
|
||||
runTests();
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ const bodySchema = z
|
||||
.strict();
|
||||
|
||||
const ensureTrailingSlash = (url: string): string => {
|
||||
return url.endsWith('/') ? url : `${url}/`;
|
||||
return url;
|
||||
};
|
||||
|
||||
export type GenerateOidcUrlResponse = {
|
||||
|
||||
@@ -23,7 +23,7 @@ import { oidcAutoProvision } from "./oidcAutoProvision";
|
||||
import license from "@server/license/license";
|
||||
|
||||
const ensureTrailingSlash = (url: string): string => {
|
||||
return url.endsWith("/") ? url : `${url}/`;
|
||||
return url;
|
||||
};
|
||||
|
||||
const paramsSchema = z
|
||||
@@ -160,7 +160,9 @@ export async function validateOidcCallback(
|
||||
);
|
||||
|
||||
const idToken = tokens.idToken();
|
||||
logger.debug("ID token", { idToken });
|
||||
const claims = arctic.decodeIdToken(idToken);
|
||||
logger.debug("ID token claims", { claims });
|
||||
|
||||
const userIdentifier = jmespath.search(
|
||||
claims,
|
||||
@@ -243,7 +245,7 @@ export async function validateOidcCallback(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"User not provisioned in the system"
|
||||
`User with username ${userIdentifier} is unprovisioned. This user must be added to an organization before logging in.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -318,8 +318,8 @@ async function updateHttpResource(
|
||||
domainId: updatePayload.domainId,
|
||||
enabled: updatePayload.enabled,
|
||||
stickySession: updatePayload.stickySession,
|
||||
tlsServerName: updatePayload.tlsServerName || null,
|
||||
setHostHeader: updatePayload.setHostHeader || null,
|
||||
tlsServerName: updatePayload.tlsServerName,
|
||||
setHostHeader: updatePayload.setHostHeader,
|
||||
fullDomain: updatePayload.fullDomain
|
||||
})
|
||||
.where(eq(resources.resourceId, resource.resourceId))
|
||||
|
||||
Reference in New Issue
Block a user