diff --git a/blueprint.yaml b/blueprint.yaml index 6854e55f..53602c26 100644 --- a/blueprint.yaml +++ b/blueprint.yaml @@ -1,8 +1,24 @@ -resources: - resource-nice-id-duce: +client-resources: + client-resource-nice-id-uno: + name: this is my resource + protocol: tcp + proxy-port: 3001 + hostname: localhost + internal-port: 3000 + site: lively-yosemite-toad + client-resource-nice-id-duce: + name: this is my resource + protocol: udp + proxy-port: 3000 + hostname: localhost + internal-port: 3000 + site: lively-yosemite-toad + +proxy-resources: + resource-nice-id-uno: name: this is my resource protocol: http - full-domain: level1.test3.example.com + full-domain: duce.test.example.com host-header: example.com tls-server-name: example.com # auth: @@ -18,6 +34,16 @@ resources: headers: - X-Example-Header: example-value - X-Another-Header: another-value + rules: + - action: allow + match: ip + value: 1.1.1.1 + - action: deny + match: cidr + value: 2.2.2.2/32 + - action: pass + match: path + value: /admin targets: - site: lively-yosemite-toad path: /path @@ -31,7 +57,7 @@ resources: pathMatchType: exact method: http port: 8001 - resource-nice-id2: + resource-nice-id-duce: name: this is other resource protocol: tcp proxy-port: 3000 diff --git a/server/lib/blueprints/parseDockerContainers.ts b/server/lib/blueprints/parseDockerContainers.ts index d00cbd73..1510e6e1 100644 --- a/server/lib/blueprints/parseDockerContainers.ts +++ b/server/lib/blueprints/parseDockerContainers.ts @@ -52,10 +52,12 @@ function getContainerPort(container: Container): number | null { } export function processContainerLabels(containers: Container[]): { - resources: { [key: string]: ResourceConfig }; + "proxy-resources": { [key: string]: ResourceConfig }; + "client-resources": { [key: string]: ResourceConfig }; } { - const result: { resources: { [key: string]: ResourceConfig } } = { - resources: {} + const result = { + "proxy-resources": {} as { [key: string]: ResourceConfig }, + "client-resources": {} as { [key: string]: ResourceConfig } }; // Process each container @@ -64,111 +66,126 @@ export function processContainerLabels(containers: Container[]): { return; } - const resourceLabels: DockerLabels = {}; + const proxyResourceLabels: DockerLabels = {}; + const clientResourceLabels: DockerLabels = {}; - // Filter labels that start with "pangolin.proxy-resources." + // Filter and separate proxy-resources and client-resources labels Object.entries(container.labels).forEach(([key, value]) => { - if (key.startsWith("pangolin.proxy-resources.") || key.startsWith("pangolin.client-resources.")) { - // remove the pangolin. prefix - const strippedKey = key.replace("pangolin.", ""); - resourceLabels[strippedKey] = value; + if (key.startsWith("pangolin.proxy-resources.")) { + // remove the pangolin.proxy- prefix to get "resources.xxx" + const strippedKey = key.replace("pangolin.proxy-", ""); + proxyResourceLabels[strippedKey] = value; + } else if (key.startsWith("pangolin.client-resources.")) { + // remove the pangolin.client- prefix to get "resources.xxx" + const strippedKey = key.replace("pangolin.client-", ""); + clientResourceLabels[strippedKey] = value; } }); - // Skip containers with no resource labels - if (Object.keys(resourceLabels).length === 0) { - return; + // Process proxy resources + if (Object.keys(proxyResourceLabels).length > 0) { + processResourceLabels(proxyResourceLabels, container, result["proxy-resources"]); } - // Parse the labels using the existing parseDockerLabels logic - const tempResult: ParsedObject = {}; - Object.entries(resourceLabels).forEach(([key, value]) => { - setNestedProperty(tempResult, key, value); - }); - - // Merge into main result - if (tempResult.resources) { - Object.entries(tempResult.resources).forEach( - ([resourceKey, resourceConfig]: [string, any]) => { - // Initialize resource if it doesn't exist - if (!result.resources[resourceKey]) { - result.resources[resourceKey] = {}; - } - - // Merge all properties except targets - Object.entries(resourceConfig).forEach( - ([propKey, propValue]) => { - if (propKey !== "targets") { - result.resources[resourceKey][propKey] = - propValue; - } - } - ); - - // Handle targets specially - if ( - resourceConfig.targets && - Array.isArray(resourceConfig.targets) - ) { - const resource = result.resources[resourceKey]; - if (resource) { - if (!resource.targets) { - resource.targets = []; - } - - resourceConfig.targets.forEach( - (target: any, targetIndex: number) => { - // check if the target is an empty object - if ( - typeof target === "object" && - Object.keys(target).length === 0 - ) { - logger.debug( - `Skipping null target at index ${targetIndex} for resource ${resourceKey}` - ); - resource.targets!.push(null); - return; - } - - // Ensure targets array is long enough - while ( - resource.targets!.length <= targetIndex - ) { - resource.targets!.push({}); - } - - // Set default hostname and port if not provided - const finalTarget = { ...target }; - if (!finalTarget.hostname) { - finalTarget.hostname = - container.name || - container.hostname; - } - if (!finalTarget.port) { - const containerPort = - getContainerPort(container); - if (containerPort !== null) { - finalTarget.port = containerPort; - } - } - - // Merge with existing target data - resource.targets![targetIndex] = { - ...resource.targets![targetIndex], - ...finalTarget - }; - } - ); - } - } - } - ); + // Process client resources + if (Object.keys(clientResourceLabels).length > 0) { + processResourceLabels(clientResourceLabels, container, result["client-resources"]); } }); return result; } +function processResourceLabels( + resourceLabels: DockerLabels, + container: Container, + targetResult: { [key: string]: ResourceConfig } +) { + // Parse the labels using the existing parseDockerLabels logic + const tempResult: ParsedObject = {}; + Object.entries(resourceLabels).forEach(([key, value]) => { + setNestedProperty(tempResult, key, value); + }); + + // Merge into target result + if (tempResult.resources) { + Object.entries(tempResult.resources).forEach( + ([resourceKey, resourceConfig]: [string, any]) => { + // Initialize resource if it doesn't exist + if (!targetResult[resourceKey]) { + targetResult[resourceKey] = {}; + } + + // Merge all properties except targets + Object.entries(resourceConfig).forEach( + ([propKey, propValue]) => { + if (propKey !== "targets") { + targetResult[resourceKey][propKey] = propValue; + } + } + ); + + // Handle targets specially + if ( + resourceConfig.targets && + Array.isArray(resourceConfig.targets) + ) { + const resource = targetResult[resourceKey]; + if (resource) { + if (!resource.targets) { + resource.targets = []; + } + + resourceConfig.targets.forEach( + (target: any, targetIndex: number) => { + // check if the target is an empty object + if ( + typeof target === "object" && + Object.keys(target).length === 0 + ) { + logger.debug( + `Skipping null target at index ${targetIndex} for resource ${resourceKey}` + ); + resource.targets!.push(null); + return; + } + + // Ensure targets array is long enough + while ( + resource.targets!.length <= targetIndex + ) { + resource.targets!.push({}); + } + + // Set default hostname and port if not provided + const finalTarget = { ...target }; + if (!finalTarget.hostname) { + finalTarget.hostname = + container.name || + container.hostname; + } + if (!finalTarget.port) { + const containerPort = + getContainerPort(container); + if (containerPort !== null) { + finalTarget.port = containerPort; + } + } + + // Merge with existing target data + resource.targets![targetIndex] = { + ...resource.targets![targetIndex], + ...finalTarget + }; + } + ); + } + } + } + ); + } +} + // // Test example // const testContainers: Container[] = [ // { diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index ecaa00cb..a66375dd 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -404,21 +404,31 @@ export async function updateProxyResources( const existingRule = existingRules[index]; if (existingRule) { if ( - existingRule.action !== rule.action || - existingRule.match !== rule.match || + existingRule.action !== getRuleAction(rule.action) || + existingRule.match !== rule.match.toUpperCase() || existingRule.value !== rule.value ) { + validateRule(rule); await trx .update(resourceRules) .set({ - action: rule.action, - match: rule.match, + action: getRuleAction(rule.action), + match: rule.match.toUpperCase(), value: rule.value }) .where( eq(resourceRules.ruleId, existingRule.ruleId) ); } + } else { + validateRule(rule); + await trx.insert(resourceRules).values({ + resourceId: existingResource.resourceId, + action: rule.action.toUpperCase(), + match: rule.match.toUpperCase(), + value: rule.value, + priority: index + 1 // start priorities at 1 + }); } } @@ -550,25 +560,11 @@ export async function updateProxyResources( } for (const [index, rule] of resourceData.rules?.entries() || []) { - if (rule.match === "cidr") { - if (!isValidCIDR(rule.value)) { - throw new Error(`Invalid CIDR provided: ${rule.value}`); - } - } else if (rule.match === "ip") { - if (!isValidIP(rule.value)) { - throw new Error(`Invalid IP provided: ${rule.value}`); - } - } else if (rule.match === "path") { - if (!isValidUrlGlobPattern(rule.value)) { - throw new Error( - `Invalid URL glob pattern: ${rule.value}` - ); - } - } + validateRule(rule); await trx.insert(resourceRules).values({ resourceId: newResource.resourceId, - action: rule.action, - match: rule.match, + action: getRuleAction(rule.action), + match: rule.match.toUpperCase(), value: rule.value, priority: index + 1 // start priorities at 1 }); @@ -586,6 +582,34 @@ export async function updateProxyResources( return results; } +function getRuleAction(input: string) { + let action = "DROP"; + if (input == "allow") { + action = "ACCEPT"; + } else if (input == "deny") { + action = "DROP"; + } else if (input == "pass") { + action = "PASS"; + } + return action; +} + +function validateRule(rule: any) { + if (rule.match === "cidr") { + if (!isValidCIDR(rule.value)) { + throw new Error(`Invalid CIDR provided: ${rule.value}`); + } + } else if (rule.match === "ip") { + if (!isValidIP(rule.value)) { + throw new Error(`Invalid IP provided: ${rule.value}`); + } + } else if (rule.match === "path") { + if (!isValidUrlGlobPattern(rule.value)) { + throw new Error(`Invalid URL glob pattern: ${rule.value}`); + } + } +} + async function syncRoleResources( resourceId: number, ssoRoles: string[], diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 786e5246..2b2ce5ec 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -173,7 +173,7 @@ export function isTargetsOnlyResource(resource: any): boolean { export const ClientResourceSchema = z.object({ name: z.string().min(2).max(100), - site: z.string().min(2).max(100), + site: z.string().min(2).max(100).optional(), protocol: z.enum(["tcp", "udp"]), "proxy-port": z.number().min(1).max(65535), "hostname": z.string().min(1).max(255),