feat(orchest-identity): tenant orchest-api host derivation + allowlist

Derives the tenant orchest-api DNS from customer_name + the
orchest-{module}-{customer_name} namespace convention, with deterministic
slug normalization and an allowlist-regex guard. TODO before prod: confirm
the exact customer_name->namespace mapping against a real token.

Co-Authored-By: WOZCODE <contact@withwoz.com>
This commit is contained in:
Rafael
2026-08-20 10:10:43 -03:00
co-authored by WOZCODE
parent a795f543f6
commit 922a34a393
2 changed files with 79 additions and 0 deletions
+43
View File
@@ -3,6 +3,9 @@ import {
moduleGateSeqid,
isModuleAllowed,
isAdmin,
tenantOrchestApiHost,
isAllowedOrchestApiHost,
namespaceModule,
} from './orchest-identity';
describe('orchest-identity mapping', () => {
@@ -31,3 +34,43 @@ describe('orchest-identity mapping', () => {
expect(isAdmin([99], [34, 99])).toBe(true);
});
});
describe('orchest-identity tenant routing', () => {
it('namespaceModule default and values', () => {
expect(namespaceModule(undefined)).toBe('intelli');
expect(namespaceModule('process')).toBe('process');
expect(namespaceModule('garbage')).toBe('intelli');
});
it('tenantOrchestApiHost builds the namespace pattern', () => {
expect(tenantOrchestApiHost('acme', 'intelli')).toBe(
'orchest-api.orchest-intelli-acme.svc.cluster.local',
);
expect(tenantOrchestApiHost('acme', 'process')).toBe(
'orchest-api.orchest-process-acme.svc.cluster.local',
);
});
it('tenantOrchestApiHost normalizes a non-slug customer_name', () => {
expect(tenantOrchestApiHost('Acme Corp', 'intelli')).toBe(
'orchest-api.orchest-intelli-acme-corp.svc.cluster.local',
);
});
it('isAllowedOrchestApiHost guards against malformed values', () => {
expect(
isAllowedOrchestApiHost(
'orchest-api.orchest-intelli-acme.svc.cluster.local',
),
).toBe(true);
expect(isAllowedOrchestApiHost('evil.example.com')).toBe(false);
expect(
isAllowedOrchestApiHost('orchest-api.orchest-intelli-.svc.cluster.local'),
).toBe(false);
expect(
isAllowedOrchestApiHost(
'orchest-api.orchest-other-acme.svc.cluster.local',
),
).toBe(false);
});
});
+36
View File
@@ -32,3 +32,39 @@ export function isModuleAllowed(perms: number[], gateSeqid: number): boolean {
export function isAdmin(perms: number[], adminSeqids: number[]): boolean {
return Array.isArray(perms) && perms.some((p) => adminSeqids.includes(p));
}
/** The `{module}` slug in the tenant namespace pattern; default 'intelli'. */
export function namespaceModule(
raw: string | undefined,
): 'intelli' | 'process' {
return raw === 'process' ? 'process' : 'intelli';
}
/**
* The in-cluster DNS of the calling tenant's orchest-api, from the tenant
* namespace convention `orchest-{module}-{customer_name}`.
*
* `customer_name` is normalized to the namespace slug shape (lowercase,
* non-[a-z0-9-] → '-') so a display-name value still yields a valid host; a
* value that is already a clean slug is unchanged. NOTE: confirm the exact
* prod `customer_name` → namespace mapping against a real token before relying
* on this in production (the `dadosferademo2` tenant is a known exception and
* is unsupported — it is being removed).
*/
export function tenantOrchestApiHost(
customerName: string,
module: string,
): string {
const slug = String(customerName)
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-');
return `orchest-api.orchest-${module}-${slug}.svc.cluster.local`;
}
const ORCHEST_API_HOST_RE =
/^orchest-api\.orchest-(intelli|process)-[a-z0-9-]+\.svc\.cluster\.local$/;
/** Defense in depth: only call an orchest-api host matching the convention. */
export function isAllowedOrchestApiHost(host: string): boolean {
return ORCHEST_API_HOST_RE.test(host);
}