@webtypen/webframez-core 0.3.107 → 0.3.109

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.
@@ -0,0 +1,30 @@
1
+ import { Model } from "../Database/Model";
2
+ import { AuthMembershipCondition } from "./AuthMembership";
3
+ export type AuthResourceGrants = {
4
+ all?: boolean;
5
+ own?: boolean;
6
+ assigned?: boolean;
7
+ };
8
+ export type AuthResourceAccessOptions = {
9
+ model: typeof Model;
10
+ /** Immutable creator reference on the resource. */
11
+ ownerField?: string;
12
+ /** Array of explicitly assigned user references (also supports paths such as members.user). */
13
+ membersField?: string;
14
+ userKey?: string;
15
+ userConditions?: readonly AuthMembershipCondition[];
16
+ resourceConditions?: readonly AuthMembershipCondition[];
17
+ };
18
+ /** Document-query authorization for all, owned and explicitly assigned resources.
19
+ * Callers must supply authenticated, server-loaded users and server-derived grants.
20
+ * Use the same filter before pagination/counts/exports and for individual mutations.
21
+ */
22
+ export declare class AuthResourceAccess {
23
+ private readonly options;
24
+ private readonly primaryKey;
25
+ private readonly userKey;
26
+ constructor(options: AuthResourceAccessOptions);
27
+ filter(user: Model, grants: AuthResourceGrants): Record<string, any>;
28
+ /** The returned model is freshly read; missing/invalid IDs never create an unbounded query. */
29
+ find<TResource extends Model = Model>(user: Model, id: unknown, grants: AuthResourceGrants): Promise<TResource | null>;
30
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.AuthResourceAccess = void 0;
13
+ const Model_1 = require("../Database/Model");
14
+ /** Document-query authorization for all, owned and explicitly assigned resources.
15
+ * Callers must supply authenticated, server-loaded users and server-derived grants.
16
+ * Use the same filter before pagination/counts/exports and for individual mutations.
17
+ */
18
+ class AuthResourceAccess {
19
+ constructor(options) {
20
+ this.options = options;
21
+ this.primaryKey = new options.model().__primaryKey;
22
+ this.userKey = options.userKey || "_id";
23
+ for (const field of [this.primaryKey, this.userKey, options.ownerField, options.membersField,
24
+ ...(options.userConditions || []).map(condition => condition.field),
25
+ ...(options.resourceConditions || []).map(condition => condition.field)]) {
26
+ if (field !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(field))
27
+ throw new Error("Invalid resource access field.");
28
+ }
29
+ if (options.ownerField && options.ownerField === options.membersField)
30
+ throw new Error("Owner and membership fields must be separate.");
31
+ }
32
+ filter(user, grants) {
33
+ const deny = { [this.primaryKey]: { $in: [] } };
34
+ if (!(user instanceof Model_1.Model))
35
+ return deny;
36
+ const userId = user[this.userKey];
37
+ if (userId === null || userId === undefined || userId === "")
38
+ return deny;
39
+ if (!(this.options.userConditions || []).every(condition => condition.operator === "="
40
+ ? user[condition.field] === condition.value : user[condition.field] !== condition.value))
41
+ return deny;
42
+ const alternatives = [];
43
+ if (grants.own === true && this.options.ownerField)
44
+ alternatives.push({ [this.options.ownerField]: { $eq: userId } });
45
+ if (grants.assigned === true && this.options.membersField)
46
+ alternatives.push({ [this.options.membersField]: { $eq: userId } });
47
+ if (grants.all !== true && !alternatives.length)
48
+ return deny;
49
+ const conditions = (this.options.resourceConditions || []).map(condition => ({
50
+ [condition.field]: { [condition.operator === "=" ? "$eq" : "$ne"]: condition.value },
51
+ }));
52
+ if (grants.all !== true)
53
+ conditions.push({ $or: alternatives });
54
+ return conditions.length ? { $and: conditions } : {};
55
+ }
56
+ /** The returned model is freshly read; missing/invalid IDs never create an unbounded query. */
57
+ find(user, id, grants) {
58
+ return __awaiter(this, void 0, void 0, function* () {
59
+ if (id === null || id === undefined || id === "" || !(user instanceof Model_1.Model))
60
+ return null;
61
+ const resourceId = yield this.options.model.objectId(id, { noExceptions: true });
62
+ if (resourceId === null || resourceId === undefined)
63
+ return null;
64
+ return yield this.options.model.where("$and", "=", [
65
+ { [this.primaryKey]: { $eq: resourceId } }, this.filter(user, grants),
66
+ ]).first();
67
+ });
68
+ }
69
+ }
70
+ exports.AuthResourceAccess = AuthResourceAccess;
@@ -9,3 +9,4 @@ export * from "./Auth";
9
9
  export * from "./PasswordReset";
10
10
  export * from "./AuthMembership";
11
11
  export * from "./AuthHandoff";
12
+ export * from "./AuthResourceAccess";
@@ -25,3 +25,4 @@ __exportStar(require("./Auth"), exports);
25
25
  __exportStar(require("./PasswordReset"), exports);
26
26
  __exportStar(require("./AuthMembership"), exports);
27
27
  __exportStar(require("./AuthHandoff"), exports);
28
+ __exportStar(require("./AuthResourceAccess"), exports);
@@ -554,10 +554,9 @@ class Datatable {
554
554
  if (!this.columns) {
555
555
  return null;
556
556
  }
557
- if (typeof this.columns === "function") {
558
- return yield this.columns(req);
559
- }
560
- return this.columns;
557
+ const columns = typeof this.columns === "function" ? yield this.columns(req) : this.columns;
558
+ // Conditional definitions are absent columns, including for saved layouts and display options.
559
+ return columns ? Object.fromEntries(Object.entries(columns).filter(([, column]) => column != null && column !== false)) : columns;
561
560
  });
562
561
  }
563
562
  getOnPressLink(req) {
@@ -584,3 +584,43 @@ checked by `isSessionAllowed` on every authentication/refresh. To enforce logout
584
584
  request, do not cache positive introspection results. This requires one authority round trip
585
585
  per check and fails closed when the authority is unavailable. No browser push is implied;
586
586
  clients detect logout when making their next authenticated request.
587
+
588
+
589
+ ## Owned and assigned resource scopes
590
+
591
+ `AuthResourceAccess` composes document filters for global, creator-owned and
592
+ explicitly assigned records. It complements `AuthMembership` (separate membership
593
+ models) with embedded administrator assignments and list/detail query parity.
594
+
595
+ ```ts
596
+ const access = new AuthResourceAccess({
597
+ model: Project,
598
+ ownerField: "created_by",
599
+ membersField: "administrators.user",
600
+ userConditions: [{ field: "is_active", operator: "!=", value: false }],
601
+ resourceConditions: [{ field: "is_deleted", operator: "!=", value: true }],
602
+ });
603
+ const grants = {
604
+ all: user.can("projects.read"),
605
+ own: user.can("projects.own.read"),
606
+ assigned: user.can("projects.assigned.read"),
607
+ };
608
+ const rows = await Project.aggregate([{ $match: access.filter(user, grants) }]);
609
+ const project = await access.find(user, req.params.id, grants);
610
+ if (!project) return res.status(403).send({ code: "forbidden" });
611
+ ```
612
+
613
+ Grants are server-derived booleans; own and assigned are a union. Missing identity,
614
+ missing references and invalid resource IDs fail closed. `all` still respects the
615
+ configured status conditions. `filter` must run before pagination, counts, exports,
616
+ selectors and lookups that could expose other records. `find` performs a fresh
617
+ lookup, so revoked assignments take effect on the next request. Neither method
618
+ caches authorization or authenticates a supplied model. User identity fields are
619
+ scalar model fields; resource mappings may use document paths.
620
+
621
+ Set creator references server-side on creation and keep them immutable. Protect
622
+ assignment changes separately from scoped editing, and never derive grants from
623
+ submitted form values. Use the filter in conditional writes where concurrent
624
+ revocation must also constrain an already running operation. Existing records
625
+ without a creator or assignment gain no scoped access. Scope membership itself
626
+ does not grant another application's login or tenant membership.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtypen/webframez-core",
3
- "version": "0.3.107",
3
+ "version": "0.3.109",
4
4
  "description": "The core package of the typescript based webframez (web-framework) by webtypen.de",
5
5
  "homepage": "https://webtypen.de/",
6
6
  "license": "MIT",