@3sixty/api-tenant-processor 0.0.0-stage → 0.20.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +24 -2
- package/dist/es/entities/tenant.js +77 -0
- package/dist/es/entities/tenant.js.map +1 -0
- package/dist/es/entities/tenantV0.js +61 -0
- package/dist/es/entities/tenantV0.js.map +1 -0
- package/dist/es/entities/tenantV1.js +77 -0
- package/dist/es/entities/tenantV1.js.map +1 -0
- package/dist/es/index.js +29 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/models/ISingleTenantProcessorConfig.js +4 -0
- package/dist/es/models/ISingleTenantProcessorConfig.js.map +1 -0
- package/dist/es/models/ISingleTenantProcessorConstructorOptions.js +2 -0
- package/dist/es/models/ISingleTenantProcessorConstructorOptions.js.map +1 -0
- package/dist/es/models/ITenantAdminServiceConfig.js +4 -0
- package/dist/es/models/ITenantAdminServiceConfig.js.map +1 -0
- package/dist/es/models/ITenantAdminServiceConstructorOptions.js +2 -0
- package/dist/es/models/ITenantAdminServiceConstructorOptions.js.map +1 -0
- package/dist/es/models/ITenantOverrideProcessorConfig.js +4 -0
- package/dist/es/models/ITenantOverrideProcessorConfig.js.map +1 -0
- package/dist/es/models/ITenantOverrideProcessorConstructorOptions.js +2 -0
- package/dist/es/models/ITenantOverrideProcessorConstructorOptions.js.map +1 -0
- package/dist/es/models/ITenantProcessorConfig.js +4 -0
- package/dist/es/models/ITenantProcessorConfig.js.map +1 -0
- package/dist/es/models/ITenantProcessorConstructorOptions.js +2 -0
- package/dist/es/models/ITenantProcessorConstructorOptions.js.map +1 -0
- package/dist/es/models/api/ITenantCreateRequest.js +2 -0
- package/dist/es/models/api/ITenantCreateRequest.js.map +1 -0
- package/dist/es/models/api/ITenantGetByApiKeyRequest.js +4 -0
- package/dist/es/models/api/ITenantGetByApiKeyRequest.js.map +1 -0
- package/dist/es/models/api/ITenantGetByIdRequest.js +4 -0
- package/dist/es/models/api/ITenantGetByIdRequest.js.map +1 -0
- package/dist/es/models/api/ITenantGetByPublicOriginRequest.js +4 -0
- package/dist/es/models/api/ITenantGetByPublicOriginRequest.js.map +1 -0
- package/dist/es/models/api/ITenantGetResponse.js +2 -0
- package/dist/es/models/api/ITenantGetResponse.js.map +1 -0
- package/dist/es/models/api/ITenantListRequest.js +4 -0
- package/dist/es/models/api/ITenantListRequest.js.map +1 -0
- package/dist/es/models/api/ITenantListResponse.js +2 -0
- package/dist/es/models/api/ITenantListResponse.js.map +1 -0
- package/dist/es/models/api/ITenantRemoveRequest.js +4 -0
- package/dist/es/models/api/ITenantRemoveRequest.js.map +1 -0
- package/dist/es/models/api/ITenantUpdateRequest.js +2 -0
- package/dist/es/models/api/ITenantUpdateRequest.js.map +1 -0
- package/dist/es/restEntryPoints.js +13 -0
- package/dist/es/restEntryPoints.js.map +1 -0
- package/dist/es/schema.js +64 -0
- package/dist/es/schema.js.map +1 -0
- package/dist/es/singleTenantProcessor.js +81 -0
- package/dist/es/singleTenantProcessor.js.map +1 -0
- package/dist/es/tenantAdminService.js +500 -0
- package/dist/es/tenantAdminService.js.map +1 -0
- package/dist/es/tenantIdContextIdHandler.js +47 -0
- package/dist/es/tenantIdContextIdHandler.js.map +1 -0
- package/dist/es/tenantOverrideProcessor.js +119 -0
- package/dist/es/tenantOverrideProcessor.js.map +1 -0
- package/dist/es/tenantProcessor.js +164 -0
- package/dist/es/tenantProcessor.js.map +1 -0
- package/dist/es/tenantRoutes.js +404 -0
- package/dist/es/tenantRoutes.js.map +1 -0
- package/dist/es/utils/tenantIdHelper.js +23 -0
- package/dist/es/utils/tenantIdHelper.js.map +1 -0
- package/dist/types/entities/tenant.d.ts +37 -0
- package/dist/types/entities/tenantV0.d.ts +29 -0
- package/dist/types/entities/tenantV1.d.ts +37 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/models/ISingleTenantProcessorConfig.d.ts +9 -0
- package/dist/types/models/ISingleTenantProcessorConstructorOptions.d.ts +10 -0
- package/dist/types/models/ITenantAdminServiceConfig.d.ts +24 -0
- package/dist/types/models/ITenantAdminServiceConstructorOptions.d.ts +15 -0
- package/dist/types/models/ITenantOverrideProcessorConfig.d.ts +13 -0
- package/dist/types/models/ITenantOverrideProcessorConstructorOptions.d.ts +15 -0
- package/dist/types/models/ITenantProcessorConfig.d.ts +18 -0
- package/dist/types/models/ITenantProcessorConstructorOptions.d.ts +15 -0
- package/dist/types/models/api/ITenantCreateRequest.d.ts +12 -0
- package/dist/types/models/api/ITenantGetByApiKeyRequest.d.ts +14 -0
- package/dist/types/models/api/ITenantGetByIdRequest.d.ts +14 -0
- package/dist/types/models/api/ITenantGetByPublicOriginRequest.d.ts +14 -0
- package/dist/types/models/api/ITenantGetResponse.d.ts +10 -0
- package/dist/types/models/api/ITenantListRequest.d.ts +27 -0
- package/dist/types/models/api/ITenantListResponse.d.ts +17 -0
- package/dist/types/models/api/ITenantRemoveRequest.d.ts +14 -0
- package/dist/types/models/api/ITenantUpdateRequest.d.ts +19 -0
- package/dist/types/restEntryPoints.d.ts +5 -0
- package/dist/types/schema.d.ts +5 -0
- package/dist/types/singleTenantProcessor.d.ts +41 -0
- package/dist/types/tenantAdminService.d.ts +109 -0
- package/dist/types/tenantIdContextIdHandler.d.ts +33 -0
- package/dist/types/tenantOverrideProcessor.d.ts +44 -0
- package/dist/types/tenantProcessor.d.ts +34 -0
- package/dist/types/tenantRoutes.d.ts +78 -0
- package/dist/types/utils/tenantIdHelper.d.ts +15 -0
- package/docs/changelog.md +0 -0
- package/docs/examples.md +81 -0
- package/docs/reference/classes/SingleTenantProcessor.md +128 -0
- package/docs/reference/classes/Tenant.md +77 -0
- package/docs/reference/classes/TenantAdminService.md +393 -0
- package/docs/reference/classes/TenantIdContextIdHandler.md +123 -0
- package/docs/reference/classes/TenantIdHelper.md +41 -0
- package/docs/reference/classes/TenantOverrideProcessor.md +119 -0
- package/docs/reference/classes/TenantProcessor.md +101 -0
- package/docs/reference/functions/generateRestRoutesTenants.md +25 -0
- package/docs/reference/functions/initSchema.md +10 -0
- package/docs/reference/functions/tenantByApiKey.md +31 -0
- package/docs/reference/functions/tenantById.md +31 -0
- package/docs/reference/functions/tenantByPublicOrigin.md +31 -0
- package/docs/reference/functions/tenantCreate.md +37 -0
- package/docs/reference/functions/tenantList.md +31 -0
- package/docs/reference/functions/tenantRemove.md +31 -0
- package/docs/reference/functions/tenantUpdate.md +31 -0
- package/docs/reference/index.md +47 -0
- package/docs/reference/interfaces/ISingleTenantProcessorConfig.md +11 -0
- package/docs/reference/interfaces/ISingleTenantProcessorConstructorOptions.md +11 -0
- package/docs/reference/interfaces/ITenantAdminServiceConfig.md +49 -0
- package/docs/reference/interfaces/ITenantAdminServiceConstructorOptions.md +25 -0
- package/docs/reference/interfaces/ITenantCreateRequest.md +17 -0
- package/docs/reference/interfaces/ITenantGetByApiKeyRequest.md +17 -0
- package/docs/reference/interfaces/ITenantGetByIdRequest.md +17 -0
- package/docs/reference/interfaces/ITenantGetByPublicOriginRequest.md +17 -0
- package/docs/reference/interfaces/ITenantGetResponse.md +11 -0
- package/docs/reference/interfaces/ITenantListRequest.md +36 -0
- package/docs/reference/interfaces/ITenantListResponse.md +23 -0
- package/docs/reference/interfaces/ITenantOverrideProcessorConfig.md +19 -0
- package/docs/reference/interfaces/ITenantOverrideProcessorConstructorOptions.md +25 -0
- package/docs/reference/interfaces/ITenantProcessorConfig.md +33 -0
- package/docs/reference/interfaces/ITenantProcessorConstructorOptions.md +25 -0
- package/docs/reference/interfaces/ITenantRemoveRequest.md +17 -0
- package/docs/reference/interfaces/ITenantUpdateRequest.md +25 -0
- package/docs/reference/variables/tagsTenants.md +5 -0
- package/locales/en.json +24 -0
- package/package.json +57 -5
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Copyright 2026 IOTA Stiftung.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
import { ForbiddenError, HttpContextIdKeys, HttpErrorHelper, ScopeHelper } from "@3sixty/api-models";
|
|
4
|
+
import { ContextIdKeys } from "@3sixty/context";
|
|
5
|
+
import { BaseError, ComponentFactory, GuardError, Is, NotFoundError } from "@3sixty/core";
|
|
6
|
+
/**
|
|
7
|
+
* Processes the overrideTenant query parameter for routes that allow tenant override.
|
|
8
|
+
* Requires the caller to hold the escalated privilege scope. Runs after AuthHeaderProcessor so
|
|
9
|
+
* authentication is always resolved in the caller's own partition first.
|
|
10
|
+
*/
|
|
11
|
+
export class TenantOverrideProcessor {
|
|
12
|
+
/**
|
|
13
|
+
* The query parameter name used to supply the override tenant ID.
|
|
14
|
+
*/
|
|
15
|
+
static OVERRIDE_TENANT_PARAM = "override-tenant";
|
|
16
|
+
/**
|
|
17
|
+
* The scope string required to perform a tenant override.
|
|
18
|
+
*/
|
|
19
|
+
static DEFAULT_ESCALATED_PRIVILEGE_SCOPE = "global-admin";
|
|
20
|
+
/**
|
|
21
|
+
* Runtime name for the class.
|
|
22
|
+
*/
|
|
23
|
+
static CLASS_NAME = "TenantOverrideProcessor";
|
|
24
|
+
/**
|
|
25
|
+
* The component used to resolve tenants, which also provides the lookup caching.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
_tenantAdminComponent;
|
|
29
|
+
/**
|
|
30
|
+
* Include the stack with errors.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
_includeErrorStack;
|
|
34
|
+
/**
|
|
35
|
+
* The scope value that grants cross-tenant access.
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
_escalatedPrivilegeScope;
|
|
39
|
+
/**
|
|
40
|
+
* Create a new instance of TenantOverrideProcessor.
|
|
41
|
+
* @param options Options for the processor.
|
|
42
|
+
*/
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this._tenantAdminComponent = ComponentFactory.get(options?.tenantAdminComponentType ?? "tenant-admin");
|
|
45
|
+
this._includeErrorStack = options?.config?.includeErrorStack ?? false;
|
|
46
|
+
this._escalatedPrivilegeScope =
|
|
47
|
+
options?.config?.escalatedPrivilegeScope ??
|
|
48
|
+
TenantOverrideProcessor.DEFAULT_ESCALATED_PRIVILEGE_SCOPE;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns the class name of the component.
|
|
52
|
+
* @returns The class name of the component.
|
|
53
|
+
*/
|
|
54
|
+
className() {
|
|
55
|
+
return TenantOverrideProcessor.CLASS_NAME;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Pre process the REST request for the specified route.
|
|
59
|
+
* @param request The incoming request.
|
|
60
|
+
* @param response The outgoing response.
|
|
61
|
+
* @param route The route to process.
|
|
62
|
+
* @param contextIds The context IDs of the request.
|
|
63
|
+
* @param processorState The state handed through the processors.
|
|
64
|
+
* @returns A promise that resolves when the tenant override has been applied or skipped.
|
|
65
|
+
*/
|
|
66
|
+
async pre(request, response, route, contextIds, processorState) {
|
|
67
|
+
if (Is.empty(route) || (route.disableTenantOverride ?? false)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const overrideTenantParam = request.query?.[TenantOverrideProcessor.OVERRIDE_TENANT_PARAM];
|
|
71
|
+
if (!Is.stringValue(overrideTenantParam)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// If a prior processor already set an error response, skip processing.
|
|
75
|
+
if (Is.notEmpty(response.statusCode)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
// Caller must hold the escalated privilege scope.
|
|
80
|
+
if (!ScopeHelper.includes(contextIds[HttpContextIdKeys.Scope], this._escalatedPrivilegeScope)) {
|
|
81
|
+
throw new ForbiddenError(TenantOverrideProcessor.CLASS_NAME, "insufficientScopeForTenantOverride");
|
|
82
|
+
}
|
|
83
|
+
// Verify the tenant exists before substituting.
|
|
84
|
+
await this.verifyTenantExists(overrideTenantParam);
|
|
85
|
+
contextIds[HttpContextIdKeys.OriginalTenant] = contextIds[ContextIdKeys.Tenant];
|
|
86
|
+
contextIds[ContextIdKeys.Tenant] = overrideTenantParam;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
const error = BaseError.fromError(err);
|
|
90
|
+
const { httpStatusCode } = HttpErrorHelper.processError(error);
|
|
91
|
+
HttpErrorHelper.buildResponse(response, error, httpStatusCode, this._includeErrorStack);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Confirm the tenant being switched to exists.
|
|
96
|
+
* @param tenantId The tenant id from the override query parameter.
|
|
97
|
+
* @throws NotFoundError if no tenant matches the id.
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
async verifyTenantExists(tenantId) {
|
|
101
|
+
let tenant;
|
|
102
|
+
try {
|
|
103
|
+
tenant = await this._tenantAdminComponent.get(tenantId);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
// An id that matches no tenant, or is not even well formed, is a not found; anything
|
|
107
|
+
// else is a genuine fault and keeps its own status code.
|
|
108
|
+
if (BaseError.isErrorName(err, NotFoundError.CLASS_NAME) ||
|
|
109
|
+
BaseError.isErrorName(err, GuardError.CLASS_NAME)) {
|
|
110
|
+
throw new NotFoundError(TenantOverrideProcessor.CLASS_NAME, "tenantNotFound", tenantId, undefined, err);
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
if (tenant.id !== tenantId) {
|
|
115
|
+
throw new NotFoundError(TenantOverrideProcessor.CLASS_NAME, "tenantNotFound", tenantId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=tenantOverrideProcessor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenantOverrideProcessor.js","sourceRoot":"","sources":["../../src/tenantOverrideProcessor.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tForbiddenError,\n\tHttpContextIdKeys,\n\tHttpErrorHelper,\n\tScopeHelper,\n\ttype IBaseRoute,\n\ttype IBaseRouteProcessor,\n\ttype IHttpResponse,\n\ttype IHttpServerRequest,\n\ttype ITenant,\n\ttype ITenantAdminComponent\n} from \"@3sixty/api-models\";\nimport { ContextIdKeys, type IContextIds } from \"@3sixty/context\";\nimport { BaseError, ComponentFactory, GuardError, Is, NotFoundError } from \"@3sixty/core\";\nimport { nameof } from \"@3sixty/nameof\";\nimport type { ITenantOverrideProcessorConstructorOptions } from \"./models/ITenantOverrideProcessorConstructorOptions.js\";\n\n/**\n * Processes the overrideTenant query parameter for routes that allow tenant override.\n * Requires the caller to hold the escalated privilege scope. Runs after AuthHeaderProcessor so\n * authentication is always resolved in the caller's own partition first.\n */\nexport class TenantOverrideProcessor implements IBaseRouteProcessor {\n\t/**\n\t * The query parameter name used to supply the override tenant ID.\n\t */\n\tpublic static readonly OVERRIDE_TENANT_PARAM: string = \"override-tenant\";\n\n\t/**\n\t * The scope string required to perform a tenant override.\n\t */\n\tpublic static readonly DEFAULT_ESCALATED_PRIVILEGE_SCOPE: string = \"global-admin\";\n\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<TenantOverrideProcessor>();\n\n\t/**\n\t * The component used to resolve tenants, which also provides the lookup caching.\n\t * @internal\n\t */\n\tprivate readonly _tenantAdminComponent: ITenantAdminComponent;\n\n\t/**\n\t * Include the stack with errors.\n\t * @internal\n\t */\n\tprivate readonly _includeErrorStack: boolean;\n\n\t/**\n\t * The scope value that grants cross-tenant access.\n\t * @internal\n\t */\n\tprivate readonly _escalatedPrivilegeScope: string;\n\n\t/**\n\t * Create a new instance of TenantOverrideProcessor.\n\t * @param options Options for the processor.\n\t */\n\tconstructor(options?: ITenantOverrideProcessorConstructorOptions) {\n\t\tthis._tenantAdminComponent = ComponentFactory.get<ITenantAdminComponent>(\n\t\t\toptions?.tenantAdminComponentType ?? \"tenant-admin\"\n\t\t);\n\t\tthis._includeErrorStack = options?.config?.includeErrorStack ?? false;\n\t\tthis._escalatedPrivilegeScope =\n\t\t\toptions?.config?.escalatedPrivilegeScope ??\n\t\t\tTenantOverrideProcessor.DEFAULT_ESCALATED_PRIVILEGE_SCOPE;\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn TenantOverrideProcessor.CLASS_NAME;\n\t}\n\n\t/**\n\t * Pre process the REST request for the specified route.\n\t * @param request The incoming request.\n\t * @param response The outgoing response.\n\t * @param route The route to process.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t * @returns A promise that resolves when the tenant override has been applied or skipped.\n\t */\n\tpublic async pre(\n\t\trequest: IHttpServerRequest,\n\t\tresponse: IHttpResponse,\n\t\troute: IBaseRoute | undefined,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<void> {\n\t\tif (Is.empty(route) || (route.disableTenantOverride ?? false)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst overrideTenantParam = request.query?.[TenantOverrideProcessor.OVERRIDE_TENANT_PARAM];\n\t\tif (!Is.stringValue(overrideTenantParam)) {\n\t\t\treturn;\n\t\t}\n\n\t\t// If a prior processor already set an error response, skip processing.\n\t\tif (Is.notEmpty(response.statusCode)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\t// Caller must hold the escalated privilege scope.\n\t\t\tif (\n\t\t\t\t!ScopeHelper.includes(contextIds[HttpContextIdKeys.Scope], this._escalatedPrivilegeScope)\n\t\t\t) {\n\t\t\t\tthrow new ForbiddenError(\n\t\t\t\t\tTenantOverrideProcessor.CLASS_NAME,\n\t\t\t\t\t\"insufficientScopeForTenantOverride\"\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Verify the tenant exists before substituting.\n\t\t\tawait this.verifyTenantExists(overrideTenantParam);\n\n\t\t\tcontextIds[HttpContextIdKeys.OriginalTenant] = contextIds[ContextIdKeys.Tenant];\n\t\t\tcontextIds[ContextIdKeys.Tenant] = overrideTenantParam;\n\t\t} catch (err) {\n\t\t\tconst error = BaseError.fromError(err);\n\t\t\tconst { httpStatusCode } = HttpErrorHelper.processError(error);\n\t\t\tHttpErrorHelper.buildResponse(response, error, httpStatusCode, this._includeErrorStack);\n\t\t}\n\t}\n\n\t/**\n\t * Confirm the tenant being switched to exists.\n\t * @param tenantId The tenant id from the override query parameter.\n\t * @throws NotFoundError if no tenant matches the id.\n\t * @internal\n\t */\n\tprivate async verifyTenantExists(tenantId: string): Promise<void> {\n\t\tlet tenant: ITenant;\n\n\t\ttry {\n\t\t\ttenant = await this._tenantAdminComponent.get(tenantId);\n\t\t} catch (err) {\n\t\t\t// An id that matches no tenant, or is not even well formed, is a not found; anything\n\t\t\t// else is a genuine fault and keeps its own status code.\n\t\t\tif (\n\t\t\t\tBaseError.isErrorName(err, NotFoundError.CLASS_NAME) ||\n\t\t\t\tBaseError.isErrorName(err, GuardError.CLASS_NAME)\n\t\t\t) {\n\t\t\t\tthrow new NotFoundError(\n\t\t\t\t\tTenantOverrideProcessor.CLASS_NAME,\n\t\t\t\t\t\"tenantNotFound\",\n\t\t\t\t\ttenantId,\n\t\t\t\t\tundefined,\n\t\t\t\t\terr\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\n\t\tif (tenant.id !== tenantId) {\n\t\t\tthrow new NotFoundError(TenantOverrideProcessor.CLASS_NAME, \"tenantNotFound\", tenantId);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Copyright 2025 IOTA Stiftung.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
import { HttpErrorHelper, HttpContextIdKeys } from "@3sixty/api-models";
|
|
4
|
+
import { ContextIdKeys } from "@3sixty/context";
|
|
5
|
+
import { BaseError, ComponentFactory, GuardError, Is, NotFoundError, UnauthorizedError } from "@3sixty/core";
|
|
6
|
+
import { HttpStatusCode } from "@3sixty/web";
|
|
7
|
+
/**
|
|
8
|
+
* Handles incoming api keys and maps them to tenant ids.
|
|
9
|
+
*/
|
|
10
|
+
export class TenantProcessor {
|
|
11
|
+
/**
|
|
12
|
+
* The default name for the api key header.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
static DEFAULT_API_KEY_NAME = "x-api-key";
|
|
16
|
+
/**
|
|
17
|
+
* Runtime name for the class.
|
|
18
|
+
*/
|
|
19
|
+
static CLASS_NAME = "TenantProcessor";
|
|
20
|
+
/**
|
|
21
|
+
* The default endpoints to match for api key processing.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
static _DEFAULT_API_KEY_ENDPOINTS = ["/login$"];
|
|
25
|
+
/**
|
|
26
|
+
* The component used to resolve tenants, which also provides the lookup caching.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
_tenantAdminComponent;
|
|
30
|
+
/**
|
|
31
|
+
* The key in the header to look for the api key.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
_apiKeyName;
|
|
35
|
+
/**
|
|
36
|
+
* The list of regexp patterns to match against the request URL to determine if the api key header should be processed.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
_apiKeyEndpoints;
|
|
40
|
+
/**
|
|
41
|
+
* Include the stack with errors.
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
_includeErrorStack;
|
|
45
|
+
/**
|
|
46
|
+
* Create a new instance of TenantProcessor.
|
|
47
|
+
* @param options Options for the processor.
|
|
48
|
+
*/
|
|
49
|
+
constructor(options) {
|
|
50
|
+
this._tenantAdminComponent = ComponentFactory.get(options?.tenantAdminComponentType ?? "tenant-admin");
|
|
51
|
+
this._apiKeyName = options?.config?.apiKeyName ?? TenantProcessor.DEFAULT_API_KEY_NAME;
|
|
52
|
+
this._apiKeyEndpoints = (options?.config?.apiKeyEndpoints ?? TenantProcessor._DEFAULT_API_KEY_ENDPOINTS).map((ep) => new RegExp(ep));
|
|
53
|
+
this._includeErrorStack = options?.config?.includeErrorStack ?? false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns the class name of the component.
|
|
57
|
+
* @returns The class name of the component.
|
|
58
|
+
*/
|
|
59
|
+
className() {
|
|
60
|
+
return TenantProcessor.CLASS_NAME;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pre process the REST request for the specified route.
|
|
64
|
+
* @param request The incoming request.
|
|
65
|
+
* @param response The outgoing response.
|
|
66
|
+
* @param route The route to process.
|
|
67
|
+
* @param contextIds The context IDs of the request.
|
|
68
|
+
* @param processorState The state handed through the processors.
|
|
69
|
+
* @returns A promise that resolves when the tenant context has been resolved and injected, or an error response set.
|
|
70
|
+
*/
|
|
71
|
+
async pre(request, response, route, contextIds, processorState) {
|
|
72
|
+
if (!Is.empty(route) && !(route.skipTenant ?? false)) {
|
|
73
|
+
try {
|
|
74
|
+
let tenant;
|
|
75
|
+
const isSkipAuth = route.skipAuth ?? false;
|
|
76
|
+
const urlPath = new URL(request.url, "http://localhost").pathname;
|
|
77
|
+
const isApiKeyEndpoint = this._apiKeyEndpoints.some(re => re.test(urlPath));
|
|
78
|
+
if (isApiKeyEndpoint) {
|
|
79
|
+
const apiKey = request.headers?.[this._apiKeyName] ?? request.query?.[this._apiKeyName];
|
|
80
|
+
if (Is.stringValue(apiKey)) {
|
|
81
|
+
tenant = await this.resolveByApiKey(apiKey);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
throw new UnauthorizedError(TenantProcessor.CLASS_NAME, "missingApiKey", {
|
|
85
|
+
keyName: this._apiKeyName
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const organizationIdQueryParam = request.query?.[ContextIdKeys.Organization];
|
|
91
|
+
if (Is.stringValue(organizationIdQueryParam)) {
|
|
92
|
+
tenant = await this.resolveByOrganizationId(organizationIdQueryParam);
|
|
93
|
+
}
|
|
94
|
+
else if (isSkipAuth) {
|
|
95
|
+
throw new UnauthorizedError(TenantProcessor.CLASS_NAME, "missingOrganizationId", {
|
|
96
|
+
paramName: ContextIdKeys.Organization
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!Is.empty(tenant)) {
|
|
101
|
+
contextIds[ContextIdKeys.Tenant] = tenant.id;
|
|
102
|
+
contextIds[ContextIdKeys.Organization] = tenant.organizationId;
|
|
103
|
+
// If the tenant has a custom public origin, we set it in the context for downstream processors to use.
|
|
104
|
+
if (Is.stringValue(tenant.publicOrigin)) {
|
|
105
|
+
contextIds[HttpContextIdKeys.PublicOrigin] = tenant.publicOrigin;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
HttpErrorHelper.buildResponse(response, BaseError.fromError(err), HttpStatusCode.unauthorized, this._includeErrorStack);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the tenant context from an api key.
|
|
116
|
+
* @param apiKey The api key sent by the caller.
|
|
117
|
+
* @returns The tenant associated with the api key.
|
|
118
|
+
* @throws UnauthorizedError if no tenant matches the api key.
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
async resolveByApiKey(apiKey) {
|
|
122
|
+
try {
|
|
123
|
+
return await this._tenantAdminComponent.getByApiKey(apiKey);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (this.isLookupFailure(err)) {
|
|
127
|
+
throw new UnauthorizedError(TenantProcessor.CLASS_NAME, "apiKeyNotFound", { key: apiKey }, err);
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the tenant context from a plain organization query param.
|
|
134
|
+
* Matches against the organization id or one of the legacy organization ids.
|
|
135
|
+
* @param organizationId The organization id from the query param.
|
|
136
|
+
* @returns The tenant associated with the organization id.
|
|
137
|
+
* @throws UnauthorizedError if no tenant matches the organization id.
|
|
138
|
+
* @internal
|
|
139
|
+
*/
|
|
140
|
+
async resolveByOrganizationId(organizationId) {
|
|
141
|
+
try {
|
|
142
|
+
return await this._tenantAdminComponent.getTenantByOrganizationId(organizationId, true);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (this.isLookupFailure(err)) {
|
|
146
|
+
throw new UnauthorizedError(TenantProcessor.CLASS_NAME, "organizationIdNotFound", { organizationId }, err);
|
|
147
|
+
}
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Determine whether a tenant lookup failure means the credential is unusable.
|
|
153
|
+
* A credential that matches no tenant, or is not even well formed, is unauthorized; anything
|
|
154
|
+
* else is a genuine fault and is passed through untouched.
|
|
155
|
+
* @param err The error raised by the tenant admin component.
|
|
156
|
+
* @returns True if the credential should be reported as unauthorized.
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
159
|
+
isLookupFailure(err) {
|
|
160
|
+
return (BaseError.isErrorName(err, NotFoundError.CLASS_NAME) ||
|
|
161
|
+
BaseError.isErrorName(err, GuardError.CLASS_NAME));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=tenantProcessor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenantProcessor.js","sourceRoot":"","sources":["../../src/tenantProcessor.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tHttpErrorHelper,\n\ttype IBaseRoute,\n\ttype IBaseRouteProcessor,\n\ttype IHttpResponse,\n\ttype IHttpServerRequest,\n\ttype ITenant,\n\ttype ITenantAdminComponent,\n\tHttpContextIdKeys\n} from \"@3sixty/api-models\";\nimport { ContextIdKeys, type IContextIds } from \"@3sixty/context\";\nimport {\n\tBaseError,\n\tComponentFactory,\n\tGuardError,\n\tIs,\n\tNotFoundError,\n\tUnauthorizedError\n} from \"@3sixty/core\";\nimport { nameof } from \"@3sixty/nameof\";\nimport { HttpStatusCode } from \"@3sixty/web\";\nimport type { ITenantProcessorConstructorOptions } from \"./models/ITenantProcessorConstructorOptions.js\";\n\n/**\n * Handles incoming api keys and maps them to tenant ids.\n */\nexport class TenantProcessor implements IBaseRouteProcessor {\n\t/**\n\t * The default name for the api key header.\n\t * @internal\n\t */\n\tpublic static readonly DEFAULT_API_KEY_NAME: string = \"x-api-key\";\n\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<TenantProcessor>();\n\n\t/**\n\t * The default endpoints to match for api key processing.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_API_KEY_ENDPOINTS: string[] = [\"/login$\"];\n\n\t/**\n\t * The component used to resolve tenants, which also provides the lookup caching.\n\t * @internal\n\t */\n\tprivate readonly _tenantAdminComponent: ITenantAdminComponent;\n\n\t/**\n\t * The key in the header to look for the api key.\n\t * @internal\n\t */\n\tprivate readonly _apiKeyName: string;\n\n\t/**\n\t * The list of regexp patterns to match against the request URL to determine if the api key header should be processed.\n\t * @internal\n\t */\n\tprivate readonly _apiKeyEndpoints: RegExp[];\n\n\t/**\n\t * Include the stack with errors.\n\t * @internal\n\t */\n\tprivate readonly _includeErrorStack: boolean;\n\n\t/**\n\t * Create a new instance of TenantProcessor.\n\t * @param options Options for the processor.\n\t */\n\tconstructor(options?: ITenantProcessorConstructorOptions) {\n\t\tthis._tenantAdminComponent = ComponentFactory.get<ITenantAdminComponent>(\n\t\t\toptions?.tenantAdminComponentType ?? \"tenant-admin\"\n\t\t);\n\t\tthis._apiKeyName = options?.config?.apiKeyName ?? TenantProcessor.DEFAULT_API_KEY_NAME;\n\t\tthis._apiKeyEndpoints = (\n\t\t\toptions?.config?.apiKeyEndpoints ?? TenantProcessor._DEFAULT_API_KEY_ENDPOINTS\n\t\t).map((ep: string) => new RegExp(ep));\n\t\tthis._includeErrorStack = options?.config?.includeErrorStack ?? false;\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn TenantProcessor.CLASS_NAME;\n\t}\n\n\t/**\n\t * Pre process the REST request for the specified route.\n\t * @param request The incoming request.\n\t * @param response The outgoing response.\n\t * @param route The route to process.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t * @returns A promise that resolves when the tenant context has been resolved and injected, or an error response set.\n\t */\n\tpublic async pre(\n\t\trequest: IHttpServerRequest,\n\t\tresponse: IHttpResponse,\n\t\troute: IBaseRoute | undefined,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<void> {\n\t\tif (!Is.empty(route) && !(route.skipTenant ?? false)) {\n\t\t\ttry {\n\t\t\t\tlet tenant;\n\t\t\t\tconst isSkipAuth = route.skipAuth ?? false;\n\t\t\t\tconst urlPath = new URL(request.url, \"http://localhost\").pathname;\n\t\t\t\tconst isApiKeyEndpoint = this._apiKeyEndpoints.some(re => re.test(urlPath));\n\n\t\t\t\tif (isApiKeyEndpoint) {\n\t\t\t\t\tconst apiKey = request.headers?.[this._apiKeyName] ?? request.query?.[this._apiKeyName];\n\t\t\t\t\tif (Is.stringValue(apiKey)) {\n\t\t\t\t\t\ttenant = await this.resolveByApiKey(apiKey);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthrow new UnauthorizedError(TenantProcessor.CLASS_NAME, \"missingApiKey\", {\n\t\t\t\t\t\t\tkeyName: this._apiKeyName\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst organizationIdQueryParam = request.query?.[ContextIdKeys.Organization];\n\t\t\t\t\tif (Is.stringValue(organizationIdQueryParam)) {\n\t\t\t\t\t\ttenant = await this.resolveByOrganizationId(organizationIdQueryParam);\n\t\t\t\t\t} else if (isSkipAuth) {\n\t\t\t\t\t\tthrow new UnauthorizedError(TenantProcessor.CLASS_NAME, \"missingOrganizationId\", {\n\t\t\t\t\t\t\tparamName: ContextIdKeys.Organization\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (!Is.empty(tenant)) {\n\t\t\t\t\tcontextIds[ContextIdKeys.Tenant] = tenant.id;\n\t\t\t\t\tcontextIds[ContextIdKeys.Organization] = tenant.organizationId;\n\n\t\t\t\t\t// If the tenant has a custom public origin, we set it in the context for downstream processors to use.\n\t\t\t\t\tif (Is.stringValue(tenant.publicOrigin)) {\n\t\t\t\t\t\tcontextIds[HttpContextIdKeys.PublicOrigin] = tenant.publicOrigin;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tHttpErrorHelper.buildResponse(\n\t\t\t\t\tresponse,\n\t\t\t\t\tBaseError.fromError(err),\n\t\t\t\t\tHttpStatusCode.unauthorized,\n\t\t\t\t\tthis._includeErrorStack\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Resolve the tenant context from an api key.\n\t * @param apiKey The api key sent by the caller.\n\t * @returns The tenant associated with the api key.\n\t * @throws UnauthorizedError if no tenant matches the api key.\n\t * @internal\n\t */\n\tprivate async resolveByApiKey(apiKey: string): Promise<ITenant> {\n\t\ttry {\n\t\t\treturn await this._tenantAdminComponent.getByApiKey(apiKey);\n\t\t} catch (err) {\n\t\t\tif (this.isLookupFailure(err)) {\n\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\tTenantProcessor.CLASS_NAME,\n\t\t\t\t\t\"apiKeyNotFound\",\n\t\t\t\t\t{ key: apiKey },\n\t\t\t\t\terr\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\t/**\n\t * Resolve the tenant context from a plain organization query param.\n\t * Matches against the organization id or one of the legacy organization ids.\n\t * @param organizationId The organization id from the query param.\n\t * @returns The tenant associated with the organization id.\n\t * @throws UnauthorizedError if no tenant matches the organization id.\n\t * @internal\n\t */\n\tprivate async resolveByOrganizationId(organizationId: string): Promise<ITenant> {\n\t\ttry {\n\t\t\treturn await this._tenantAdminComponent.getTenantByOrganizationId(organizationId, true);\n\t\t} catch (err) {\n\t\t\tif (this.isLookupFailure(err)) {\n\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\tTenantProcessor.CLASS_NAME,\n\t\t\t\t\t\"organizationIdNotFound\",\n\t\t\t\t\t{ organizationId },\n\t\t\t\t\terr\n\t\t\t\t);\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\t/**\n\t * Determine whether a tenant lookup failure means the credential is unusable.\n\t * A credential that matches no tenant, or is not even well formed, is unauthorized; anything\n\t * else is a genuine fault and is passed through untouched.\n\t * @param err The error raised by the tenant admin component.\n\t * @returns True if the credential should be reported as unauthorized.\n\t * @internal\n\t */\n\tprivate isLookupFailure(err: unknown): boolean {\n\t\treturn (\n\t\t\tBaseError.isErrorName(err, NotFoundError.CLASS_NAME) ||\n\t\t\tBaseError.isErrorName(err, GuardError.CLASS_NAME)\n\t\t);\n\t}\n}\n"]}
|