aws-service-stack 0.18.447 → 0.18.449
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/README.md +79 -7
- package/dist/controller/base-controller.d.ts +2 -1
- package/dist/controller/base-controller.d.ts.map +1 -1
- package/dist/controller/base-controller.js +64 -15
- package/dist/controller/base-controller.js.map +1 -1
- package/dist/controller/controller-api.d.ts +13 -2
- package/dist/controller/controller-api.d.ts.map +1 -1
- package/dist/controller/controller-api.js +77 -10
- package/dist/controller/controller-api.js.map +1 -1
- package/dist/controller/controller-request.d.ts +3 -0
- package/dist/controller/controller-request.d.ts.map +1 -1
- package/dist/controller/controller-request.js +5 -0
- package/dist/controller/controller-request.js.map +1 -1
- package/dist/controller/controller-role.d.ts +2 -1
- package/dist/controller/controller-role.d.ts.map +1 -1
- package/dist/controller/controller-role.js +12 -3
- package/dist/controller/controller-role.js.map +1 -1
- package/dist/model/base.config.d.ts +4 -1
- package/dist/model/base.config.d.ts.map +1 -1
- package/dist/model/base.config.js +1 -0
- package/dist/model/base.config.js.map +1 -1
- package/dist/model/base.model.d.ts +3 -0
- package/dist/model/base.model.d.ts.map +1 -1
- package/dist/model/base.model.js.map +1 -1
- package/dist/model/caller.model.d.ts +22 -0
- package/dist/model/caller.model.d.ts.map +1 -0
- package/dist/model/caller.model.js +12 -0
- package/dist/model/caller.model.js.map +1 -0
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.d.ts.map +1 -1
- package/dist/model/index.js +1 -0
- package/dist/model/index.js.map +1 -1
- package/dist/model/validation.model.d.ts +15 -1
- package/dist/model/validation.model.d.ts.map +1 -1
- package/dist/model/validation.model.js +3 -1
- package/dist/model/validation.model.js.map +1 -1
- package/dist/utils/auth/caller.d.ts +9 -0
- package/dist/utils/auth/caller.d.ts.map +1 -0
- package/dist/utils/auth/caller.js +108 -0
- package/dist/utils/auth/caller.js.map +1 -0
- package/dist/utils/auth/policy-view.d.ts +42 -0
- package/dist/utils/auth/policy-view.d.ts.map +1 -0
- package/dist/utils/auth/policy-view.js +170 -0
- package/dist/utils/auth/policy-view.js.map +1 -0
- package/dist/utils/http/api-event.d.ts +23 -0
- package/dist/utils/http/api-event.d.ts.map +1 -0
- package/dist/utils/http/api-event.js +84 -0
- package/dist/utils/http/api-event.js.map +1 -0
- package/dist/utils/http/http.util.d.ts +12 -2
- package/dist/utils/http/http.util.d.ts.map +1 -1
- package/dist/utils/http/http.util.js +50 -24
- package/dist/utils/http/http.util.js.map +1 -1
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/opensearch/opensearch.parser.aggs.d.ts +2 -1
- package/dist/utils/opensearch/opensearch.parser.aggs.d.ts.map +1 -1
- package/dist/utils/opensearch/opensearch.parser.aggs.js +38 -4
- package/dist/utils/opensearch/opensearch.parser.aggs.js.map +1 -1
- package/dist/utils/opensearch/opensearch.parser.d.ts +1 -1
- package/dist/utils/reflection.util.d.ts +4 -0
- package/dist/utils/reflection.util.d.ts.map +1 -1
- package/dist/utils/reflection.util.js +17 -0
- package/dist/utils/reflection.util.js.map +1 -1
- package/package.json +60 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CallerKind = void 0;
|
|
4
|
+
var CallerKind;
|
|
5
|
+
(function (CallerKind) {
|
|
6
|
+
CallerKind["GUEST"] = "guest";
|
|
7
|
+
CallerKind["USER"] = "user";
|
|
8
|
+
CallerKind["RETAILER"] = "retailer";
|
|
9
|
+
CallerKind["SUPPLIER"] = "supplier";
|
|
10
|
+
CallerKind["ADMIN"] = "admin";
|
|
11
|
+
})(CallerKind || (exports.CallerKind = CallerKind = {}));
|
|
12
|
+
//# sourceMappingURL=caller.model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caller.model.js","sourceRoot":"","sources":["../../src/model/caller.model.ts"],"names":[],"mappings":";;;AAAA,IAAY,UAMX;AAND,WAAY,UAAU;IACpB,6BAAe,CAAA;IACf,2BAAa,CAAA;IACb,mCAAqB,CAAA;IACrB,mCAAqB,CAAA;IACrB,6BAAe,CAAA;AACjB,CAAC,EANW,UAAU,aAAV,UAAU,GAAV,UAAU,QAMrB","sourcesContent":["export enum CallerKind {\n GUEST = \"guest\",\n USER = \"user\",\n RETAILER = \"retailer\",\n SUPPLIER = \"supplier\",\n ADMIN = \"admin\",\n}\n\nexport interface CallerPoolConfig {\n userPoolId: string;\n kind: CallerKind.USER | CallerKind.RETAILER | CallerKind.SUPPLIER | CallerKind.ADMIN;\n /** null/undefined = any client of the pool */\n clientIds?: string[] | null;\n}\n\nexport interface Caller {\n kind: CallerKind;\n claims?: Record<string, any>;\n}\n\nexport interface CallerVerifier {\n /** Returns the verified JWT payload (incl. iss). Throws when invalid. */\n verify(token: string): Promise<Record<string, any>>;\n}\n"]}
|
package/dist/model/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/model/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/model/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,cAAc,gBAAgB,CAAC"}
|
package/dist/model/index.js
CHANGED
|
@@ -22,4 +22,5 @@ __exportStar(require("./filter.model"), exports);
|
|
|
22
22
|
__exportStar(require("./validation.model"), exports);
|
|
23
23
|
__exportStar(require("./dynamodb.model"), exports);
|
|
24
24
|
__exportStar(require("./base.config"), exports);
|
|
25
|
+
__exportStar(require("./caller.model"), exports);
|
|
25
26
|
//# sourceMappingURL=index.js.map
|
package/dist/model/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/model/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,uDAAqC;AACrC,+CAA6B;AAC7B,+CAA6B;AAC7B,sDAAoC;AACpC,iDAA+B;AAC/B,qDAAmC;AACnC,mDAAiC;AACjC,gDAA8B","sourcesContent":["export * from \"./cognito-user.model\";\nexport * from \"./http.model\";\nexport * from \"./base.model\";\nexport * from \"./date-string.model\";\nexport * from \"./filter.model\";\nexport * from \"./validation.model\";\nexport * from \"./dynamodb.model\";\nexport * from \"./base.config\";\nexport { Permission as RolePermission } from \"./role.model\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/model/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,uDAAqC;AACrC,+CAA6B;AAC7B,+CAA6B;AAC7B,sDAAoC;AACpC,iDAA+B;AAC/B,qDAAmC;AACnC,mDAAiC;AACjC,gDAA8B;AAE9B,iDAA+B","sourcesContent":["export * from \"./cognito-user.model\";\nexport * from \"./http.model\";\nexport * from \"./base.model\";\nexport * from \"./date-string.model\";\nexport * from \"./filter.model\";\nexport * from \"./validation.model\";\nexport * from \"./dynamodb.model\";\nexport * from \"./base.config\";\nexport { Permission as RolePermission } from \"./role.model\";\nexport * from \"./caller.model\";\n"]}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { HttpMethod } from "../index.js";
|
|
1
|
+
import type { HttpMethod } from "../index.js";
|
|
2
|
+
import type { CallerKind } from "./caller.model";
|
|
2
3
|
export interface ResponseFields {
|
|
3
4
|
include?: string[];
|
|
4
5
|
exclude?: string[];
|
|
5
6
|
}
|
|
7
|
+
export interface ClaimRef {
|
|
8
|
+
claim: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const claim: (name: string) => ClaimRef;
|
|
11
|
+
export type ViewFilterValue = string | number | boolean | string[] | ClaimRef;
|
|
12
|
+
export interface PolicyView {
|
|
13
|
+
filter?: Record<string, ViewFilterValue>;
|
|
14
|
+
response?: ResponseFields;
|
|
15
|
+
}
|
|
6
16
|
/** Path-based permission definition */
|
|
7
17
|
export interface EndpointPolicy {
|
|
8
18
|
method: HttpMethod;
|
|
@@ -10,6 +20,10 @@ export interface EndpointPolicy {
|
|
|
10
20
|
access?: Access[];
|
|
11
21
|
validator?: any;
|
|
12
22
|
response?: ResponseFields;
|
|
23
|
+
/** Who may call; absent = legacy `access` rules */
|
|
24
|
+
kinds?: CallerKind[];
|
|
25
|
+
/** Per-caller-kind forced filter and response fields */
|
|
26
|
+
view?: Partial<Record<CallerKind, PolicyView>>;
|
|
13
27
|
}
|
|
14
28
|
export declare enum RequestType {
|
|
15
29
|
GUEST = "guest",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.model.d.ts","sourceRoot":"","sources":["../../src/model/validation.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"validation.model.d.ts","sourceRoot":"","sources":["../../src/model/validation.model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,KAAK,SAAU,MAAM,KAAG,QAA6B,CAAC;AAEnE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,GAAG,QAAQ,CAAC;AAE9E,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,mDAAmD;IACnD,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,wDAAwD;IACxD,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;CAChD;AAED,oBAAY,WAAW;IACrB,KAAK,UAAU;IACf,IAAI,SAAS;IACb,KAAK,UAAU;IACf,MAAM,WAAW;CAClB;AAED,oBAAY,MAAM;IAChB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;IACf,MAAM,WAAW;CAClB"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Access = exports.RequestType = void 0;
|
|
3
|
+
exports.Access = exports.RequestType = exports.claim = void 0;
|
|
4
|
+
const claim = (name) => ({ claim: name });
|
|
5
|
+
exports.claim = claim;
|
|
4
6
|
var RequestType;
|
|
5
7
|
(function (RequestType) {
|
|
6
8
|
RequestType["GUEST"] = "guest";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.model.js","sourceRoot":"","sources":["../../src/model/validation.model.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"validation.model.js","sourceRoot":"","sources":["../../src/model/validation.model.ts"],"names":[],"mappings":";;;AAYO,MAAM,KAAK,GAAG,CAAC,IAAY,EAAY,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAAtD,QAAA,KAAK,GAAL,KAAK,CAAiD;AAsBnE,IAAY,WAKX;AALD,WAAY,WAAW;IACrB,8BAAe,CAAA;IACf,4BAAa,CAAA;IACb,8BAAe,CAAA;IACf,gCAAiB,CAAA;AACnB,CAAC,EALW,WAAW,aAAX,WAAW,GAAX,WAAW,QAKtB;AAED,IAAY,MAMX;AAND,WAAY,MAAM;IAChB,2BAAiB,CAAA;IACjB,uBAAa,CAAA;IACb,yBAAe,CAAA;IACf,yBAAe,CAAA;IACf,2BAAiB,CAAA;AACnB,CAAC,EANW,MAAM,aAAN,MAAM,GAAN,MAAM,QAMjB","sourcesContent":["import type { HttpMethod } from \"@chinggis/core\";\nimport type { CallerKind } from \"./caller.model\";\n\nexport interface ResponseFields {\n include?: string[];\n exclude?: string[];\n}\n\nexport interface ClaimRef {\n claim: string;\n}\n\nexport const claim = (name: string): ClaimRef => ({ claim: name });\n\nexport type ViewFilterValue = string | number | boolean | string[] | ClaimRef;\n\nexport interface PolicyView {\n filter?: Record<string, ViewFilterValue>;\n response?: ResponseFields;\n}\n\n/** Path-based permission definition */\nexport interface EndpointPolicy {\n method: HttpMethod;\n path: string;\n access?: Access[];\n validator?: any;\n response?: ResponseFields;\n /** Who may call; absent = legacy `access` rules */\n kinds?: CallerKind[];\n /** Per-caller-kind forced filter and response fields */\n view?: Partial<Record<CallerKind, PolicyView>>;\n}\n\nexport enum RequestType {\n GUEST = \"guest\",\n USER = \"user\",\n ADMIN = \"admin\",\n SYSTEM = \"system\",\n}\n\nexport enum Access {\n PUBLIC = \"public\",\n USER = \"user\",\n OWNER = \"owner\",\n ADMIN = \"admin\",\n SYSTEM = \"system\",\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Caller, type CallerPoolConfig, type CallerVerifier } from "../../model/caller.model";
|
|
2
|
+
/** Throws a 500 config error unless `pools` is an array of `{ userPoolId: string, kind: user|retailer|supplier|admin }` */
|
|
3
|
+
export declare function validateCallerPools(pools: unknown): CallerPoolConfig[];
|
|
4
|
+
/** Verifier for the pools (cached per pool config); an invalid pool config is a 500 config error */
|
|
5
|
+
export declare function createCallerVerifier(pools: CallerPoolConfig[]): CallerVerifier;
|
|
6
|
+
/** Parses and validates the CALLER_POOLS env value (memoized per string); invalid -> 500 config error */
|
|
7
|
+
export declare function parseCallerPoolsEnv(value: string | undefined): CallerPoolConfig[] | undefined;
|
|
8
|
+
export declare function resolveCaller(authorization: string | undefined, pools: CallerPoolConfig[], adminGroups: string[], verifier?: CallerVerifier): Promise<Caller>;
|
|
9
|
+
//# sourceMappingURL=caller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caller.d.ts","sourceRoot":"","sources":["../../../src/utils/auth/caller.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,MAAM,EAAc,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAQ/G,2HAA2H;AAC3H,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,EAAE,CAQtE;AAED,oGAAoG;AACpG,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,cAAc,CAiB9E;AAED,yGAAyG;AACzG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,EAAE,GAAG,SAAS,CAa7F;AAmBD,wBAAsB,aAAa,CACjC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,KAAK,EAAE,gBAAgB,EAAE,EACzB,WAAW,EAAE,MAAM,EAAE,EACrB,QAAQ,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,MAAM,CAAC,CA2BjB"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateCallerPools = validateCallerPools;
|
|
4
|
+
exports.createCallerVerifier = createCallerVerifier;
|
|
5
|
+
exports.parseCallerPoolsEnv = parseCallerPoolsEnv;
|
|
6
|
+
exports.resolveCaller = resolveCaller;
|
|
7
|
+
const aws_jwt_verify_1 = require("aws-jwt-verify");
|
|
8
|
+
const error_1 = require("aws-jwt-verify/error");
|
|
9
|
+
const errors_1 = require("../../exception/errors");
|
|
10
|
+
const exception_handler_1 = require("../../exception/exception-handler");
|
|
11
|
+
const caller_model_1 = require("../../model/caller.model");
|
|
12
|
+
const verifierCache = new Map();
|
|
13
|
+
const poolsEnvCache = new Map();
|
|
14
|
+
const POOL_KINDS = [caller_model_1.CallerKind.USER, caller_model_1.CallerKind.RETAILER, caller_model_1.CallerKind.SUPPLIER, caller_model_1.CallerKind.ADMIN];
|
|
15
|
+
const configError = (message) => new errors_1.ErrorHttp(exception_handler_1.HTTP.INTERNAL_SERVER_ERROR, `Invalid CALLER_POOLS: ${message}`);
|
|
16
|
+
/** Throws a 500 config error unless `pools` is an array of `{ userPoolId: string, kind: user|retailer|supplier|admin }` */
|
|
17
|
+
function validateCallerPools(pools) {
|
|
18
|
+
if (!Array.isArray(pools))
|
|
19
|
+
throw configError("expected an array");
|
|
20
|
+
pools.forEach((pool, i) => {
|
|
21
|
+
if (!pool || typeof pool !== "object")
|
|
22
|
+
throw configError(`entry ${i} is not an object`);
|
|
23
|
+
if (typeof pool.userPoolId !== "string" || !pool.userPoolId)
|
|
24
|
+
throw configError(`entry ${i} needs userPoolId`);
|
|
25
|
+
if (!POOL_KINDS.includes(pool.kind))
|
|
26
|
+
throw configError(`entry ${i} has kind ${pool.kind}`);
|
|
27
|
+
});
|
|
28
|
+
return pools;
|
|
29
|
+
}
|
|
30
|
+
/** Verifier for the pools (cached per pool config); an invalid pool config is a 500 config error */
|
|
31
|
+
function createCallerVerifier(pools) {
|
|
32
|
+
const key = JSON.stringify(pools);
|
|
33
|
+
let verifier = verifierCache.get(key);
|
|
34
|
+
if (!verifier) {
|
|
35
|
+
validateCallerPools(pools);
|
|
36
|
+
let jwtVerifier;
|
|
37
|
+
try {
|
|
38
|
+
jwtVerifier = aws_jwt_verify_1.CognitoJwtVerifier.create(pools.map((p) => ({ userPoolId: p.userPoolId, tokenUse: null, clientId: p.clientIds ?? null })));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw configError(err?.message ?? String(err));
|
|
42
|
+
}
|
|
43
|
+
verifier = { verify: async (token) => (await jwtVerifier.verify(token)) };
|
|
44
|
+
verifierCache.set(key, verifier);
|
|
45
|
+
}
|
|
46
|
+
return verifier;
|
|
47
|
+
}
|
|
48
|
+
/** Parses and validates the CALLER_POOLS env value (memoized per string); invalid -> 500 config error */
|
|
49
|
+
function parseCallerPoolsEnv(value) {
|
|
50
|
+
if (!value?.trim())
|
|
51
|
+
return undefined;
|
|
52
|
+
const cached = poolsEnvCache.get(value);
|
|
53
|
+
if (cached)
|
|
54
|
+
return cached;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(value);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw configError("not valid JSON");
|
|
61
|
+
}
|
|
62
|
+
const pools = validateCallerPools(parsed);
|
|
63
|
+
poolsEnvCache.set(value, pools);
|
|
64
|
+
return pools;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* JWKS/network trouble is 503, anything else an invalid token (401). That includes ParameterValidationError:
|
|
68
|
+
* at verify time the multi-issuer verifier throws it for a foreign or missing `iss` (client input).
|
|
69
|
+
* Config errors surface earlier, from createCallerVerifier, outside the verify try (500).
|
|
70
|
+
*/
|
|
71
|
+
function verifyFailure(err) {
|
|
72
|
+
if (err instanceof error_1.FetchError ||
|
|
73
|
+
err instanceof error_1.NonRetryableFetchError ||
|
|
74
|
+
err instanceof error_1.JwksNotAvailableInCacheError ||
|
|
75
|
+
err instanceof error_1.JwksValidationError) {
|
|
76
|
+
return new errors_1.ErrorHttp(exception_handler_1.HTTP.SERVICE_UNAVAILABLE, "Token keys are not available");
|
|
77
|
+
}
|
|
78
|
+
return new errors_1.ErrorHttp(exception_handler_1.HTTP.UNAUTHORIZED, "Invalid token");
|
|
79
|
+
}
|
|
80
|
+
async function resolveCaller(authorization, pools, adminGroups, verifier) {
|
|
81
|
+
if (!authorization)
|
|
82
|
+
return { kind: caller_model_1.CallerKind.GUEST };
|
|
83
|
+
const token = authorization.replace(/^Bearer\s+/i, "").trim();
|
|
84
|
+
// Built outside the try: a config error must surface as 500, not as an invalid token
|
|
85
|
+
const tokenVerifier = verifier ?? createCallerVerifier(pools);
|
|
86
|
+
let claims;
|
|
87
|
+
try {
|
|
88
|
+
claims = await tokenVerifier.verify(token);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
throw verifyFailure(err);
|
|
92
|
+
}
|
|
93
|
+
const poolId = String(claims.iss ?? "")
|
|
94
|
+
.split("/")
|
|
95
|
+
.pop();
|
|
96
|
+
const pool = pools.find((p) => p.userPoolId === poolId);
|
|
97
|
+
if (!pool)
|
|
98
|
+
throw new errors_1.ErrorHttp(exception_handler_1.HTTP.UNAUTHORIZED, "Unknown token issuer");
|
|
99
|
+
if (pool.kind === caller_model_1.CallerKind.ADMIN) {
|
|
100
|
+
const raw = claims["cognito:groups"];
|
|
101
|
+
const groups = Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
|
|
102
|
+
const allowed = adminGroups.map((g) => g.toLowerCase());
|
|
103
|
+
const isAdmin = groups.some((g) => allowed.includes(String(g).toLowerCase()));
|
|
104
|
+
return { kind: isAdmin ? caller_model_1.CallerKind.ADMIN : caller_model_1.CallerKind.USER, claims };
|
|
105
|
+
}
|
|
106
|
+
return { kind: pool.kind, claims };
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=caller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caller.js","sourceRoot":"","sources":["../../../src/utils/auth/caller.ts"],"names":[],"mappings":"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","sourcesContent":["import { CognitoJwtVerifier } from \"aws-jwt-verify\";\nimport {\n FetchError,\n JwksNotAvailableInCacheError,\n JwksValidationError,\n NonRetryableFetchError,\n} from \"aws-jwt-verify/error\";\nimport { ErrorHttp } from \"../../exception/errors\";\nimport { HTTP } from \"../../exception/exception-handler\";\nimport { type Caller, CallerKind, type CallerPoolConfig, type CallerVerifier } from \"../../model/caller.model\";\n\nconst verifierCache = new Map<string, CallerVerifier>();\nconst poolsEnvCache = new Map<string, CallerPoolConfig[]>();\nconst POOL_KINDS: string[] = [CallerKind.USER, CallerKind.RETAILER, CallerKind.SUPPLIER, CallerKind.ADMIN];\n\nconst configError = (message: string) => new ErrorHttp(HTTP.INTERNAL_SERVER_ERROR, `Invalid CALLER_POOLS: ${message}`);\n\n/** Throws a 500 config error unless `pools` is an array of `{ userPoolId: string, kind: user|retailer|supplier|admin }` */\nexport function validateCallerPools(pools: unknown): CallerPoolConfig[] {\n if (!Array.isArray(pools)) throw configError(\"expected an array\");\n pools.forEach((pool, i) => {\n if (!pool || typeof pool !== \"object\") throw configError(`entry ${i} is not an object`);\n if (typeof pool.userPoolId !== \"string\" || !pool.userPoolId) throw configError(`entry ${i} needs userPoolId`);\n if (!POOL_KINDS.includes(pool.kind)) throw configError(`entry ${i} has kind ${pool.kind}`);\n });\n return pools as CallerPoolConfig[];\n}\n\n/** Verifier for the pools (cached per pool config); an invalid pool config is a 500 config error */\nexport function createCallerVerifier(pools: CallerPoolConfig[]): CallerVerifier {\n const key = JSON.stringify(pools);\n let verifier = verifierCache.get(key);\n if (!verifier) {\n validateCallerPools(pools);\n let jwtVerifier: { verify: (token: string) => Promise<unknown> };\n try {\n jwtVerifier = CognitoJwtVerifier.create(\n pools.map((p) => ({ userPoolId: p.userPoolId, tokenUse: null, clientId: p.clientIds ?? null })),\n );\n } catch (err) {\n throw configError(err?.message ?? String(err));\n }\n verifier = { verify: async (token) => (await jwtVerifier.verify(token)) as Record<string, any> };\n verifierCache.set(key, verifier);\n }\n return verifier;\n}\n\n/** Parses and validates the CALLER_POOLS env value (memoized per string); invalid -> 500 config error */\nexport function parseCallerPoolsEnv(value: string | undefined): CallerPoolConfig[] | undefined {\n if (!value?.trim()) return undefined;\n const cached = poolsEnvCache.get(value);\n if (cached) return cached;\n let parsed: unknown;\n try {\n parsed = JSON.parse(value);\n } catch {\n throw configError(\"not valid JSON\");\n }\n const pools = validateCallerPools(parsed);\n poolsEnvCache.set(value, pools);\n return pools;\n}\n\n/**\n * JWKS/network trouble is 503, anything else an invalid token (401). That includes ParameterValidationError:\n * at verify time the multi-issuer verifier throws it for a foreign or missing `iss` (client input).\n * Config errors surface earlier, from createCallerVerifier, outside the verify try (500).\n */\nfunction verifyFailure(err: unknown): ErrorHttp {\n if (\n err instanceof FetchError ||\n err instanceof NonRetryableFetchError ||\n err instanceof JwksNotAvailableInCacheError ||\n err instanceof JwksValidationError\n ) {\n return new ErrorHttp(HTTP.SERVICE_UNAVAILABLE, \"Token keys are not available\");\n }\n return new ErrorHttp(HTTP.UNAUTHORIZED, \"Invalid token\");\n}\n\nexport async function resolveCaller(\n authorization: string | undefined,\n pools: CallerPoolConfig[],\n adminGroups: string[],\n verifier?: CallerVerifier,\n): Promise<Caller> {\n if (!authorization) return { kind: CallerKind.GUEST };\n const token = authorization.replace(/^Bearer\\s+/i, \"\").trim();\n\n // Built outside the try: a config error must surface as 500, not as an invalid token\n const tokenVerifier = verifier ?? createCallerVerifier(pools);\n let claims: Record<string, any>;\n try {\n claims = await tokenVerifier.verify(token);\n } catch (err) {\n throw verifyFailure(err);\n }\n\n const poolId = String(claims.iss ?? \"\")\n .split(\"/\")\n .pop();\n const pool = pools.find((p) => p.userPoolId === poolId);\n if (!pool) throw new ErrorHttp(HTTP.UNAUTHORIZED, \"Unknown token issuer\");\n\n if (pool.kind === CallerKind.ADMIN) {\n const raw = claims[\"cognito:groups\"];\n const groups: string[] = Array.isArray(raw) ? raw : typeof raw === \"string\" ? [raw] : [];\n const allowed = adminGroups.map((g) => g.toLowerCase());\n const isAdmin = groups.some((g) => allowed.includes(String(g).toLowerCase()));\n return { kind: isAdmin ? CallerKind.ADMIN : CallerKind.USER, claims };\n }\n return { kind: pool.kind, claims };\n}\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Caller } from "../../model/caller.model";
|
|
2
|
+
import type { Filter } from "../../model/filter.model";
|
|
3
|
+
import type { EndpointPolicy, ResponseFields } from "../../model/validation.model";
|
|
4
|
+
/** Caller-mode config check: a policy `view` only takes effect together with `kinds` (500 otherwise) */
|
|
5
|
+
export declare function assertViewHasKinds(policy: EndpointPolicy | undefined): void;
|
|
6
|
+
/** Applies the caller kind check and the per-kind view (forced filters, response fields). Pure. */
|
|
7
|
+
export declare function applyPolicyView(policy: EndpointPolicy, caller: Caller, filter: Filter): {
|
|
8
|
+
filter: Filter;
|
|
9
|
+
response?: ResponseFields;
|
|
10
|
+
};
|
|
11
|
+
/** Entity owner field names (subset of EntityConfig) that the service maps the owner aliases onto */
|
|
12
|
+
export interface OwnerFieldNames {
|
|
13
|
+
OWNER_ID_FIELD_NAME?: string;
|
|
14
|
+
OWNER_PARENT_ID_FIELD_NAME?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* When the caller's view forces filter keys, drops client selectors that could bypass or replace them:
|
|
18
|
+
* `indexName`/`indexValue`/`indexSortName`/`indexSortValue` (index choice), range filters on a forced key,
|
|
19
|
+
* `rangeFilterBy/Min/Max` on a forced key, and every key whose base field is a forced key
|
|
20
|
+
* (`min_K`, `max_K`, `K__<op>`, `K.keyword*`, `K` + Min|Max|From|To|Start|End).
|
|
21
|
+
* When a forced key is an owner field (`profileId`, `ownerParentId`, `ownerId`, `owner.OWNER_ID_FIELD_NAME`,
|
|
22
|
+
* `owner.OWNER_PARENT_ID_FIELD_NAME`), all other owner aliases/fields are dropped from the client filter too,
|
|
23
|
+
* because the service maps them onto the owner field after the view and would overwrite the forced value. Pure.
|
|
24
|
+
*/
|
|
25
|
+
export declare function stripForcedSelectors(policy: EndpointPolicy, caller: Caller, filter: Filter, owner?: OwnerFieldNames): Filter;
|
|
26
|
+
/**
|
|
27
|
+
* OpenSearch `search/query*` routes only read `field__op` keys, so a plain forced key would be ignored.
|
|
28
|
+
* Moves every forced key onto `K.keyword__eq` (scalar) or `K.keyword__in` (array, comma-joined) and removes
|
|
29
|
+
* every other client key for that field (`K`, `K__*`, `K.*`). Pure.
|
|
30
|
+
*/
|
|
31
|
+
export declare function applyForcedSearchQuery(filter: Filter, forced: Record<string, any>): Filter;
|
|
32
|
+
/** Resolved forced filter of the caller's view; empty when the view forces nothing. Throws like applyPolicyView. */
|
|
33
|
+
export declare function resolveForcedFilter(policy: EndpointPolicy, caller: Caller): Record<string, any>;
|
|
34
|
+
/** Item matches a forced filter: an array value means membership, anything else equality on the item's field */
|
|
35
|
+
export declare function matchesForcedFilter(item: Record<string, any> | null | undefined, forced: Record<string, any>): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Guards a POST/PUT/PATCH body under a forced view: a body value for a forced key must match it (403 otherwise);
|
|
38
|
+
* on POST a missing scalar forced key is set from the forced value (array-valued keys are not injected).
|
|
39
|
+
* Returns the (possibly created) body; other methods and non-object bodies are returned unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export declare function guardForcedBody(method: string, body: unknown, forced: Record<string, any>): unknown;
|
|
42
|
+
//# sourceMappingURL=policy-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-view.d.ts","sourceRoot":"","sources":["../../../src/utils/auth/policy-view.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAY,cAAc,EAAE,cAAc,EAAmB,MAAM,8BAA8B,CAAC;AAa9G,wGAAwG;AACxG,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAI3E;AAED,mGAAmG;AACnG,wBAAgB,eAAe,CAC7B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,CAyB/C;AAaD,qGAAqG;AACrG,MAAM,WAAW,eAAe;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAYD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,eAAe,GACtB,MAAM,CA2BR;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAU1F;AAED,oHAAoH;AACpH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAK/F;AAED,gHAAgH;AAChH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAAG,SAAS,EAC5C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,OAAO,CAKT;AAID;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAgBnG"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertViewHasKinds = assertViewHasKinds;
|
|
4
|
+
exports.applyPolicyView = applyPolicyView;
|
|
5
|
+
exports.stripForcedSelectors = stripForcedSelectors;
|
|
6
|
+
exports.applyForcedSearchQuery = applyForcedSearchQuery;
|
|
7
|
+
exports.resolveForcedFilter = resolveForcedFilter;
|
|
8
|
+
exports.matchesForcedFilter = matchesForcedFilter;
|
|
9
|
+
exports.guardForcedBody = guardForcedBody;
|
|
10
|
+
const errors_1 = require("../../exception/errors");
|
|
11
|
+
const exception_handler_1 = require("../../exception/exception-handler");
|
|
12
|
+
const reflection_util_1 = require("../reflection.util");
|
|
13
|
+
const isClaimRef = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && typeof v.claim === "string";
|
|
14
|
+
/**
|
|
15
|
+
* A claim value that would act as a query operator: DynamoDB treats `*x*`, `x*`, `!x` and `a,b` as
|
|
16
|
+
* contains / begins_with / not-equal / IN, OpenSearch treats `*` and `,` as wildcard and list separators.
|
|
17
|
+
*/
|
|
18
|
+
function isOperatorValue(value) {
|
|
19
|
+
return typeof value === "string" && (value.includes("*") || value.includes(",") || value.startsWith("!"));
|
|
20
|
+
}
|
|
21
|
+
/** Caller-mode config check: a policy `view` only takes effect together with `kinds` (500 otherwise) */
|
|
22
|
+
function assertViewHasKinds(policy) {
|
|
23
|
+
if (policy?.view && !policy.kinds) {
|
|
24
|
+
throw new errors_1.ErrorHttp(exception_handler_1.HTTP.INTERNAL_SERVER_ERROR, "policy view requires kinds");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Applies the caller kind check and the per-kind view (forced filters, response fields). Pure. */
|
|
28
|
+
function applyPolicyView(policy, caller, filter) {
|
|
29
|
+
if (policy.kinds && !policy.kinds.includes(caller.kind)) {
|
|
30
|
+
throw new errors_1.ErrorHttp(exception_handler_1.HTTP.FORBIDDEN, "Caller kind is not allowed");
|
|
31
|
+
}
|
|
32
|
+
const view = policy.view?.[caller.kind];
|
|
33
|
+
if (!view)
|
|
34
|
+
return { filter: { ...filter }, response: policy.response };
|
|
35
|
+
const forced = {};
|
|
36
|
+
for (const [key, value] of Object.entries(view.filter ?? {})) {
|
|
37
|
+
if (isClaimRef(value)) {
|
|
38
|
+
const resolved = caller.claims?.[value.claim];
|
|
39
|
+
if (resolved === undefined || resolved === null || resolved === "") {
|
|
40
|
+
throw new errors_1.ErrorHttp(exception_handler_1.HTTP.FORBIDDEN, `Missing claim ${value.claim}`);
|
|
41
|
+
}
|
|
42
|
+
// A claim must be a literal: `*`, `,` and a leading `!` would turn the forced equality into an operator
|
|
43
|
+
if (isOperatorValue(resolved)) {
|
|
44
|
+
throw new errors_1.ErrorHttp(exception_handler_1.HTTP.FORBIDDEN, `Claim ${value.claim} contains a query operator`);
|
|
45
|
+
}
|
|
46
|
+
forced[key] = resolved;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Forced arrays are IN lists: marked so DynamoDB index selection never joins them into a key
|
|
50
|
+
forced[key] = Array.isArray(value) ? (0, reflection_util_1.markForcedIn)([...value]) : value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { filter: { ...filter, ...forced }, response: view.response ?? policy.response };
|
|
54
|
+
}
|
|
55
|
+
const RANGE_SUFFIXES = ["Min", "Max", "From", "To", "Start", "End"];
|
|
56
|
+
/** True when a client filter key selects on forced field `forced` other than by plain equality on `forced` itself */
|
|
57
|
+
function isSelectorOf(key, forced) {
|
|
58
|
+
const lower = key.toLowerCase();
|
|
59
|
+
const forcedLower = forced.toLowerCase();
|
|
60
|
+
if (lower === `min_${forcedLower}` || lower === `max_${forcedLower}`)
|
|
61
|
+
return true;
|
|
62
|
+
if (key.startsWith(`${forced}__`) || key.startsWith(`${forced}.`))
|
|
63
|
+
return true;
|
|
64
|
+
return RANGE_SUFFIXES.some((suffix) => key === `${forced}${suffix}`);
|
|
65
|
+
}
|
|
66
|
+
/** Public owner aliases that the service (`parseOwnerFields`) rewrites onto the configured owner fields */
|
|
67
|
+
const OWNER_ALIASES = ["profileId", "ownerParentId", "ownerId"];
|
|
68
|
+
/** Owner alias and owner field names; the service may copy a client value from one onto another */
|
|
69
|
+
function ownerTargets(owner) {
|
|
70
|
+
return [...OWNER_ALIASES, owner?.OWNER_ID_FIELD_NAME, owner?.OWNER_PARENT_ID_FIELD_NAME].filter((name) => !!name);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* When the caller's view forces filter keys, drops client selectors that could bypass or replace them:
|
|
74
|
+
* `indexName`/`indexValue`/`indexSortName`/`indexSortValue` (index choice), range filters on a forced key,
|
|
75
|
+
* `rangeFilterBy/Min/Max` on a forced key, and every key whose base field is a forced key
|
|
76
|
+
* (`min_K`, `max_K`, `K__<op>`, `K.keyword*`, `K` + Min|Max|From|To|Start|End).
|
|
77
|
+
* When a forced key is an owner field (`profileId`, `ownerParentId`, `ownerId`, `owner.OWNER_ID_FIELD_NAME`,
|
|
78
|
+
* `owner.OWNER_PARENT_ID_FIELD_NAME`), all other owner aliases/fields are dropped from the client filter too,
|
|
79
|
+
* because the service maps them onto the owner field after the view and would overwrite the forced value. Pure.
|
|
80
|
+
*/
|
|
81
|
+
function stripForcedSelectors(policy, caller, filter, owner) {
|
|
82
|
+
const forcedKeys = Object.keys(policy.view?.[caller.kind]?.filter ?? {});
|
|
83
|
+
const result = { ...(filter ?? {}) };
|
|
84
|
+
if (!forcedKeys.length)
|
|
85
|
+
return result;
|
|
86
|
+
const owners = ownerTargets(owner);
|
|
87
|
+
if (forcedKeys.some((key) => owners.includes(key))) {
|
|
88
|
+
for (const name of owners)
|
|
89
|
+
if (!forcedKeys.includes(name))
|
|
90
|
+
delete result[name];
|
|
91
|
+
}
|
|
92
|
+
delete result.indexName;
|
|
93
|
+
delete result.indexValue;
|
|
94
|
+
delete result.indexSortName;
|
|
95
|
+
delete result.indexSortValue;
|
|
96
|
+
if (forcedKeys.includes(result.rangeFilterBy)) {
|
|
97
|
+
delete result.rangeFilterBy;
|
|
98
|
+
delete result.rangeFilterMin;
|
|
99
|
+
delete result.rangeFilterMax;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(result.rangeFilters)) {
|
|
102
|
+
result.rangeFilters = result.rangeFilters.filter((rf) => !forcedKeys.includes(rf.fieldName));
|
|
103
|
+
if (!result.rangeFilters.length)
|
|
104
|
+
delete result.rangeFilters;
|
|
105
|
+
}
|
|
106
|
+
for (const key of Object.keys(result)) {
|
|
107
|
+
if (forcedKeys.some((forced) => isSelectorOf(key, forced)))
|
|
108
|
+
delete result[key];
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* OpenSearch `search/query*` routes only read `field__op` keys, so a plain forced key would be ignored.
|
|
114
|
+
* Moves every forced key onto `K.keyword__eq` (scalar) or `K.keyword__in` (array, comma-joined) and removes
|
|
115
|
+
* every other client key for that field (`K`, `K__*`, `K.*`). Pure.
|
|
116
|
+
*/
|
|
117
|
+
function applyForcedSearchQuery(filter, forced) {
|
|
118
|
+
const result = { ...(filter ?? {}) };
|
|
119
|
+
for (const [key, value] of Object.entries(forced)) {
|
|
120
|
+
for (const name of Object.keys(result)) {
|
|
121
|
+
if (name === key || name.startsWith(`${key}__`) || name.startsWith(`${key}.`))
|
|
122
|
+
delete result[name];
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(value))
|
|
125
|
+
result[`${key}.keyword__in`] = value.map(String).join(",");
|
|
126
|
+
else
|
|
127
|
+
result[`${key}.keyword__eq`] = value;
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
/** Resolved forced filter of the caller's view; empty when the view forces nothing. Throws like applyPolicyView. */
|
|
132
|
+
function resolveForcedFilter(policy, caller) {
|
|
133
|
+
const forcedKeys = Object.keys(policy.view?.[caller.kind]?.filter ?? {});
|
|
134
|
+
if (!forcedKeys.length)
|
|
135
|
+
return {};
|
|
136
|
+
const { filter } = applyPolicyView(policy, caller, {});
|
|
137
|
+
return Object.fromEntries(forcedKeys.map((key) => [key, filter[key]]));
|
|
138
|
+
}
|
|
139
|
+
/** Item matches a forced filter: an array value means membership, anything else equality on the item's field */
|
|
140
|
+
function matchesForcedFilter(item, forced) {
|
|
141
|
+
if (!item)
|
|
142
|
+
return false;
|
|
143
|
+
return Object.entries(forced).every(([key, value]) => Array.isArray(value) ? value.includes(item[key]) : item[key] === value);
|
|
144
|
+
}
|
|
145
|
+
const BODY_METHODS = ["POST", "PUT", "PATCH"];
|
|
146
|
+
/**
|
|
147
|
+
* Guards a POST/PUT/PATCH body under a forced view: a body value for a forced key must match it (403 otherwise);
|
|
148
|
+
* on POST a missing scalar forced key is set from the forced value (array-valued keys are not injected).
|
|
149
|
+
* Returns the (possibly created) body; other methods and non-object bodies are returned unchanged.
|
|
150
|
+
*/
|
|
151
|
+
function guardForcedBody(method, body, forced) {
|
|
152
|
+
const upper = String(method ?? "").toUpperCase();
|
|
153
|
+
if (!BODY_METHODS.includes(upper) || !Object.keys(forced).length)
|
|
154
|
+
return body;
|
|
155
|
+
if (body !== undefined && body !== null && (typeof body !== "object" || Array.isArray(body)))
|
|
156
|
+
return body;
|
|
157
|
+
const target = (body ?? {});
|
|
158
|
+
for (const [key, value] of Object.entries(forced)) {
|
|
159
|
+
if (target[key] !== undefined) {
|
|
160
|
+
if (!matchesForcedFilter({ [key]: target[key] }, { [key]: value })) {
|
|
161
|
+
throw new errors_1.ErrorHttp(exception_handler_1.HTTP.FORBIDDEN, `Field ${key} is outside the caller's view`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (upper === "POST" && !Array.isArray(value)) {
|
|
165
|
+
target[key] = value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return body ?? (upper === "POST" && Object.keys(target).length ? target : body);
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=policy-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-view.js","sourceRoot":"","sources":["../../../src/utils/auth/policy-view.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorHttp } from \"../../exception/errors\";\nimport { HTTP } from \"../../exception/exception-handler\";\nimport type { Caller } from \"../../model/caller.model\";\nimport { markForcedIn } from \"../reflection.util\";\nimport type { Filter } from \"../../model/filter.model\";\nimport type { ClaimRef, EndpointPolicy, ResponseFields, ViewFilterValue } from \"../../model/validation.model\";\n\nconst isClaimRef = (v: ViewFilterValue): v is ClaimRef =>\n typeof v === \"object\" && v !== null && !Array.isArray(v) && typeof (v as ClaimRef).claim === \"string\";\n\n/**\n * A claim value that would act as a query operator: DynamoDB treats `*x*`, `x*`, `!x` and `a,b` as\n * contains / begins_with / not-equal / IN, OpenSearch treats `*` and `,` as wildcard and list separators.\n */\nfunction isOperatorValue(value: unknown): boolean {\n return typeof value === \"string\" && (value.includes(\"*\") || value.includes(\",\") || value.startsWith(\"!\"));\n}\n\n/** Caller-mode config check: a policy `view` only takes effect together with `kinds` (500 otherwise) */\nexport function assertViewHasKinds(policy: EndpointPolicy | undefined): void {\n if (policy?.view && !policy.kinds) {\n throw new ErrorHttp(HTTP.INTERNAL_SERVER_ERROR, \"policy view requires kinds\");\n }\n}\n\n/** Applies the caller kind check and the per-kind view (forced filters, response fields). Pure. */\nexport function applyPolicyView(\n policy: EndpointPolicy,\n caller: Caller,\n filter: Filter,\n): { filter: Filter; response?: ResponseFields } {\n if (policy.kinds && !policy.kinds.includes(caller.kind)) {\n throw new ErrorHttp(HTTP.FORBIDDEN, \"Caller kind is not allowed\");\n }\n const view = policy.view?.[caller.kind];\n if (!view) return { filter: { ...filter }, response: policy.response };\n\n const forced: Record<string, any> = {};\n for (const [key, value] of Object.entries(view.filter ?? {})) {\n if (isClaimRef(value)) {\n const resolved = caller.claims?.[value.claim];\n if (resolved === undefined || resolved === null || resolved === \"\") {\n throw new ErrorHttp(HTTP.FORBIDDEN, `Missing claim ${value.claim}`);\n }\n // A claim must be a literal: `*`, `,` and a leading `!` would turn the forced equality into an operator\n if (isOperatorValue(resolved)) {\n throw new ErrorHttp(HTTP.FORBIDDEN, `Claim ${value.claim} contains a query operator`);\n }\n forced[key] = resolved;\n } else {\n // Forced arrays are IN lists: marked so DynamoDB index selection never joins them into a key\n forced[key] = Array.isArray(value) ? markForcedIn([...value]) : value;\n }\n }\n return { filter: { ...filter, ...forced }, response: view.response ?? policy.response };\n}\n\nconst RANGE_SUFFIXES = [\"Min\", \"Max\", \"From\", \"To\", \"Start\", \"End\"];\n\n/** True when a client filter key selects on forced field `forced` other than by plain equality on `forced` itself */\nfunction isSelectorOf(key: string, forced: string): boolean {\n const lower = key.toLowerCase();\n const forcedLower = forced.toLowerCase();\n if (lower === `min_${forcedLower}` || lower === `max_${forcedLower}`) return true;\n if (key.startsWith(`${forced}__`) || key.startsWith(`${forced}.`)) return true;\n return RANGE_SUFFIXES.some((suffix) => key === `${forced}${suffix}`);\n}\n\n/** Entity owner field names (subset of EntityConfig) that the service maps the owner aliases onto */\nexport interface OwnerFieldNames {\n OWNER_ID_FIELD_NAME?: string;\n OWNER_PARENT_ID_FIELD_NAME?: string;\n}\n\n/** Public owner aliases that the service (`parseOwnerFields`) rewrites onto the configured owner fields */\nconst OWNER_ALIASES = [\"profileId\", \"ownerParentId\", \"ownerId\"];\n\n/** Owner alias and owner field names; the service may copy a client value from one onto another */\nfunction ownerTargets(owner?: OwnerFieldNames): string[] {\n return [...OWNER_ALIASES, owner?.OWNER_ID_FIELD_NAME, owner?.OWNER_PARENT_ID_FIELD_NAME].filter(\n (name): name is string => !!name,\n );\n}\n\n/**\n * When the caller's view forces filter keys, drops client selectors that could bypass or replace them:\n * `indexName`/`indexValue`/`indexSortName`/`indexSortValue` (index choice), range filters on a forced key,\n * `rangeFilterBy/Min/Max` on a forced key, and every key whose base field is a forced key\n * (`min_K`, `max_K`, `K__<op>`, `K.keyword*`, `K` + Min|Max|From|To|Start|End).\n * When a forced key is an owner field (`profileId`, `ownerParentId`, `ownerId`, `owner.OWNER_ID_FIELD_NAME`,\n * `owner.OWNER_PARENT_ID_FIELD_NAME`), all other owner aliases/fields are dropped from the client filter too,\n * because the service maps them onto the owner field after the view and would overwrite the forced value. Pure.\n */\nexport function stripForcedSelectors(\n policy: EndpointPolicy,\n caller: Caller,\n filter: Filter,\n owner?: OwnerFieldNames,\n): Filter {\n const forcedKeys = Object.keys(policy.view?.[caller.kind]?.filter ?? {});\n const result: Filter = { ...(filter ?? {}) };\n if (!forcedKeys.length) return result;\n\n const owners = ownerTargets(owner);\n if (forcedKeys.some((key) => owners.includes(key))) {\n for (const name of owners) if (!forcedKeys.includes(name)) delete result[name];\n }\n\n delete result.indexName;\n delete result.indexValue;\n delete result.indexSortName;\n delete result.indexSortValue;\n if (forcedKeys.includes(result.rangeFilterBy)) {\n delete result.rangeFilterBy;\n delete result.rangeFilterMin;\n delete result.rangeFilterMax;\n }\n if (Array.isArray(result.rangeFilters)) {\n result.rangeFilters = result.rangeFilters.filter((rf) => !forcedKeys.includes(rf.fieldName));\n if (!result.rangeFilters.length) delete result.rangeFilters;\n }\n for (const key of Object.keys(result)) {\n if (forcedKeys.some((forced) => isSelectorOf(key, forced))) delete result[key];\n }\n return result;\n}\n\n/**\n * OpenSearch `search/query*` routes only read `field__op` keys, so a plain forced key would be ignored.\n * Moves every forced key onto `K.keyword__eq` (scalar) or `K.keyword__in` (array, comma-joined) and removes\n * every other client key for that field (`K`, `K__*`, `K.*`). Pure.\n */\nexport function applyForcedSearchQuery(filter: Filter, forced: Record<string, any>): Filter {\n const result: Filter = { ...(filter ?? {}) };\n for (const [key, value] of Object.entries(forced)) {\n for (const name of Object.keys(result)) {\n if (name === key || name.startsWith(`${key}__`) || name.startsWith(`${key}.`)) delete result[name];\n }\n if (Array.isArray(value)) result[`${key}.keyword__in`] = value.map(String).join(\",\");\n else result[`${key}.keyword__eq`] = value;\n }\n return result;\n}\n\n/** Resolved forced filter of the caller's view; empty when the view forces nothing. Throws like applyPolicyView. */\nexport function resolveForcedFilter(policy: EndpointPolicy, caller: Caller): Record<string, any> {\n const forcedKeys = Object.keys(policy.view?.[caller.kind]?.filter ?? {});\n if (!forcedKeys.length) return {};\n const { filter } = applyPolicyView(policy, caller, {});\n return Object.fromEntries(forcedKeys.map((key) => [key, filter[key]]));\n}\n\n/** Item matches a forced filter: an array value means membership, anything else equality on the item's field */\nexport function matchesForcedFilter(\n item: Record<string, any> | null | undefined,\n forced: Record<string, any>,\n): boolean {\n if (!item) return false;\n return Object.entries(forced).every(([key, value]) =>\n Array.isArray(value) ? value.includes(item[key]) : item[key] === value,\n );\n}\n\nconst BODY_METHODS = [\"POST\", \"PUT\", \"PATCH\"];\n\n/**\n * Guards a POST/PUT/PATCH body under a forced view: a body value for a forced key must match it (403 otherwise);\n * on POST a missing scalar forced key is set from the forced value (array-valued keys are not injected).\n * Returns the (possibly created) body; other methods and non-object bodies are returned unchanged.\n */\nexport function guardForcedBody(method: string, body: unknown, forced: Record<string, any>): unknown {\n const upper = String(method ?? \"\").toUpperCase();\n if (!BODY_METHODS.includes(upper) || !Object.keys(forced).length) return body;\n if (body !== undefined && body !== null && (typeof body !== \"object\" || Array.isArray(body))) return body;\n\n const target = (body ?? {}) as Record<string, any>;\n for (const [key, value] of Object.entries(forced)) {\n if (target[key] !== undefined) {\n if (!matchesForcedFilter({ [key]: target[key] }, { [key]: value })) {\n throw new ErrorHttp(HTTP.FORBIDDEN, `Field ${key} is outside the caller's view`);\n }\n } else if (upper === \"POST\" && !Array.isArray(value)) {\n target[key] = value;\n }\n }\n return body ?? (upper === \"POST\" && Object.keys(target).length ? target : body);\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HttpMethod } from "../../model/index.js";
|
|
2
|
+
import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from "aws-lambda";
|
|
3
|
+
export type ApiEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
|
|
4
|
+
export interface NormalizedApiEvent {
|
|
5
|
+
version: "1.0" | "2.0";
|
|
6
|
+
method: HttpMethod;
|
|
7
|
+
/** Route template with leading slash, e.g. "/entities/{id}" */
|
|
8
|
+
routePath: string;
|
|
9
|
+
rawPath: string;
|
|
10
|
+
/** Header names are lower-cased */
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
query: Record<string, string>;
|
|
13
|
+
multiQuery: Record<string, string[]>;
|
|
14
|
+
pathParameters: Record<string, string>;
|
|
15
|
+
/** base64 already decoded */
|
|
16
|
+
body?: string;
|
|
17
|
+
/** REST API (v1) requestContext.authorizer.claims only */
|
|
18
|
+
restClaims?: Record<string, any>;
|
|
19
|
+
raw: ApiEvent;
|
|
20
|
+
}
|
|
21
|
+
export declare function isHttpApiEvent(event: ApiEvent): event is APIGatewayProxyEventV2;
|
|
22
|
+
export declare function normalizeApiEvent(event: ApiEvent): NormalizedApiEvent;
|
|
23
|
+
//# sourceMappingURL=api-event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-event.d.ts","sourceRoot":"","sources":["../../../src/utils/http/api-event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE/E,MAAM,MAAM,QAAQ,GAAG,oBAAoB,GAAG,sBAAsB,CAAC;AAErE,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,KAAK,GAAG,KAAK,CAAC;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACrC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAE1D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,GAAG,EAAE,QAAQ,CAAC;CACf;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,KAAK,IAAI,sBAAsB,CAE/E;AAuBD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,GAAG,kBAAkB,CAsDrE"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isHttpApiEvent = isHttpApiEvent;
|
|
4
|
+
exports.normalizeApiEvent = normalizeApiEvent;
|
|
5
|
+
function isHttpApiEvent(event) {
|
|
6
|
+
return event.version === "2.0";
|
|
7
|
+
}
|
|
8
|
+
function lowerCaseKeys(headers) {
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
11
|
+
if (value !== undefined)
|
|
12
|
+
result[key.toLowerCase()] = value;
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
function definedEntries(record) {
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const [key, value] of Object.entries(record ?? {})) {
|
|
19
|
+
if (value !== undefined)
|
|
20
|
+
result[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function decodeBody(body, isBase64Encoded) {
|
|
25
|
+
if (body === null || body === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
return isBase64Encoded ? Buffer.from(body, "base64").toString("utf-8") : body;
|
|
28
|
+
}
|
|
29
|
+
function normalizeApiEvent(event) {
|
|
30
|
+
if (isHttpApiEvent(event)) {
|
|
31
|
+
const query = definedEntries(event.queryStringParameters);
|
|
32
|
+
const multiQuery = {};
|
|
33
|
+
if (event.rawQueryString) {
|
|
34
|
+
const params = new URLSearchParams(event.rawQueryString);
|
|
35
|
+
for (const key of new Set(params.keys()))
|
|
36
|
+
multiQuery[key] = params.getAll(key);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
for (const [key, value] of Object.entries(query))
|
|
40
|
+
multiQuery[key] = value.split(",");
|
|
41
|
+
}
|
|
42
|
+
const routeKey = event.routeKey ?? "";
|
|
43
|
+
const spaceIdx = routeKey.indexOf(" ");
|
|
44
|
+
const routePath = routeKey === "$default" || spaceIdx < 0 ? event.rawPath : routeKey.slice(spaceIdx + 1);
|
|
45
|
+
return {
|
|
46
|
+
version: "2.0",
|
|
47
|
+
method: event.requestContext.http.method,
|
|
48
|
+
routePath,
|
|
49
|
+
rawPath: event.rawPath,
|
|
50
|
+
headers: lowerCaseKeys(event.headers),
|
|
51
|
+
query,
|
|
52
|
+
multiQuery,
|
|
53
|
+
pathParameters: definedEntries(event.pathParameters),
|
|
54
|
+
body: decodeBody(event.body, event.isBase64Encoded),
|
|
55
|
+
raw: event,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const multiQuery = {};
|
|
59
|
+
for (const [key, values] of Object.entries(event.multiValueQueryStringParameters ?? {})) {
|
|
60
|
+
if (values)
|
|
61
|
+
multiQuery[key] = values;
|
|
62
|
+
}
|
|
63
|
+
const query = definedEntries(event.queryStringParameters);
|
|
64
|
+
for (const [key, value] of Object.entries(query)) {
|
|
65
|
+
if (!(key in multiQuery))
|
|
66
|
+
multiQuery[key] = [value];
|
|
67
|
+
}
|
|
68
|
+
const resourcePath = event.requestContext?.resourcePath ?? event.resource ?? "";
|
|
69
|
+
const claims = event.requestContext?.authorizer?.claims;
|
|
70
|
+
return {
|
|
71
|
+
version: "1.0",
|
|
72
|
+
method: event.httpMethod,
|
|
73
|
+
routePath: resourcePath.startsWith("/") ? resourcePath : `/${resourcePath}`,
|
|
74
|
+
rawPath: event.path,
|
|
75
|
+
headers: lowerCaseKeys(event.headers),
|
|
76
|
+
query,
|
|
77
|
+
multiQuery,
|
|
78
|
+
pathParameters: definedEntries(event.pathParameters),
|
|
79
|
+
body: decodeBody(event.body, event.isBase64Encoded),
|
|
80
|
+
restClaims: claims ?? undefined,
|
|
81
|
+
raw: event,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=api-event.js.map
|