From caded23b5152ce9c3e178d3e18a58a18dbced5f6 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 9 May 2025 17:37:55 -0400 Subject: [PATCH] allow root path --- server/lib/validators.ts | 4 + server/routers/badger/verifySession.test.ts | 97 ++++++++++++++++--- .../resources/[resourceId]/proxy/page.tsx | 6 ++ .../resources/[resourceId]/rules/page.tsx | 1 - 4 files changed, 96 insertions(+), 12 deletions(-) diff --git a/server/lib/validators.ts b/server/lib/validators.ts index e33c918..50ff567 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -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; diff --git a/server/routers/badger/verifySession.test.ts b/server/routers/badger/verifySession.test.ts index 0a459dc..b0ad987 100644 --- a/server/routers/badger/verifySession.test.ts +++ b/server/routers/badger/verifySession.test.ts @@ -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); -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index deefd80..ddf255e 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -798,6 +798,12 @@ export default function ReverseProxyTargets(props: { type="submit" variant="outlinePrimary" className="mt-6" + disabled={ + !( + addTargetForm.getValues("ip") && + addTargetForm.getValues("port") + ) + } > Add Target diff --git a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx index 2a9fa00..f7b3914 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx @@ -64,7 +64,6 @@ import { InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; -import { Separator } from "@app/components/ui/separator"; import { InfoPopup } from "@app/components/ui/info-popup"; import { isValidCIDR,