@o3co/auth.policy-verifier.builtins 0.4.0 → 0.6.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.
@@ -26,6 +26,12 @@ import type { ReadonlyAttributes, Rule } from "@o3co/auth.policy-verifier.core";
26
26
  * literal halves around the `*` still compare exactly and case-sensitively.
27
27
  * Multiple wildcards are rejected outright because the two-part split would
28
28
  * silently drop segments and over-grant.
29
+ *
30
+ * The two halves must not overlap in the required permission (#180): a grant
31
+ * of `"posts.*.read"` does not match `"posts.read"`, where the single `.`
32
+ * would satisfy `startsWith` and `endsWith` at once. The wildcard may match
33
+ * the empty string, but each character of the required permission counts
34
+ * toward at most one half.
29
35
  */
30
36
  export declare class HasPermission implements Rule {
31
37
  private permission;
@@ -1 +1 @@
1
- {"version":3,"file":"HasPermission.d.mts","sourceRoot":"","sources":["../../src/rules/HasPermission.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAQ,IAAI,EAAE,MAAM,iCAAiC,CAAC;AAGtF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,aAAc,YAAW,IAAI;IAK7B,OAAO,CAAC,UAAU;IAJ9B,QAAQ,CAAC,QAAQ,gBAAgB;IACjC,QAAQ,CAAC,IAAI,mBAAmB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEL,UAAU,EAAE,MAAM;IAItC,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO;IAU1C,OAAO,CAAC,KAAK;CAoBb"}
1
+ {"version":3,"file":"HasPermission.d.mts","sourceRoot":"","sources":["../../src/rules/HasPermission.mts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAQ,IAAI,EAAE,MAAM,iCAAiC,CAAC;AAGtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,aAAc,YAAW,IAAI;IAK7B,OAAO,CAAC,UAAU;IAJ9B,QAAQ,CAAC,QAAQ,gBAAgB;IACjC,QAAQ,CAAC,IAAI,mBAAmB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEL,UAAU,EAAE,MAAM;IAItC,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO;IAoB1C,OAAO,CAAC,KAAK;CA0Bb"}
@@ -28,6 +28,12 @@ import { ATTR_PERMISSIONS, ATTR_ROLES } from "@o3co/auth.policy-verifier.core";
28
28
  * literal halves around the `*` still compare exactly and case-sensitively.
29
29
  * Multiple wildcards are rejected outright because the two-part split would
30
30
  * silently drop segments and over-grant.
31
+ *
32
+ * The two halves must not overlap in the required permission (#180): a grant
33
+ * of `"posts.*.read"` does not match `"posts.read"`, where the single `.`
34
+ * would satisfy `startsWith` and `endsWith` at once. The wildcard may match
35
+ * the empty string, but each character of the required permission counts
36
+ * toward at most one half.
31
37
  */
32
38
  export class HasPermission {
33
39
  permission;
@@ -39,10 +45,20 @@ export class HasPermission {
39
45
  this.message = `User does not have required permission: ${permission}`;
40
46
  }
41
47
  verify(attrs) {
42
- const direct = attrs.get(ATTR_PERMISSIONS) ?? [];
43
- const fromRoles = (attrs.get(ATTR_ROLES) ?? []).flatMap((role) => role.permissions);
44
- const all = [...direct, ...fromRoles];
45
- return all.some((p) => this.match(p, this.permission));
48
+ // Nothing about these values is taken on the cast's word (#180): both
49
+ // arrive from collectors, and a store-backed one can hand back anything
50
+ // — a non-array under either key, a role whose `permissions` is missing
51
+ // or not an array, non-string entries. Every malformed shape is ignored
52
+ // rather than half-honoured: an entry-level guard alone still throws on
53
+ // a non-array container, and spreading a bare string under
54
+ // `permissions` would splay it into characters, letting a
55
+ // one-character requirement match a value that was never a grant.
56
+ const direct = attrs.get(ATTR_PERMISSIONS);
57
+ const roles = attrs.get(ATTR_ROLES);
58
+ const fromRoles = (Array.isArray(roles) ? roles : []).flatMap((role) => Array.isArray(role?.permissions) ? role.permissions : []);
59
+ const all = [...(Array.isArray(direct) ? direct : []), ...fromRoles];
60
+ // Non-string entries never match and never throw, as in HasScope (#116).
61
+ return all.some((p) => typeof p === "string" && this.match(p, this.permission));
46
62
  }
47
63
  match(permission, required) {
48
64
  if (permission === "*")
@@ -60,6 +76,13 @@ export class HasPermission {
60
76
  if ((permission.match(/\*/g) ?? []).length > 1)
61
77
  return false;
62
78
  const [prefix, suffix] = permission.split("*");
79
+ // The halves must not overlap in `required` (#180): "posts.*.read"
80
+ // must not match "posts.read", where the single "." satisfies both
81
+ // startsWith and endsWith. Length is the whole check — the wildcard
82
+ // may match the empty string, but a character may only count toward
83
+ // one half.
84
+ if (required.length < prefix.length + suffix.length)
85
+ return false;
63
86
  return (!prefix || required.startsWith(prefix)) && (!suffix || required.endsWith(suffix));
64
87
  }
65
88
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/auth.policy-verifier.builtins",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "engines": {
@@ -31,12 +31,12 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "@o3co/auth.policy-verifier.core": "0.4.0"
34
+ "@o3co/auth.policy-verifier.core": "0.6.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@types/node": "^26.2.0",
38
- "@vitest/coverage-v8": "^4.1.10",
39
- "vitest": "^4.1.10"
37
+ "@types/node": "^26.4.0",
38
+ "@vitest/coverage-v8": "^4.1.11",
39
+ "vitest": "^4.1.11"
40
40
  },
41
41
  "scripts": {
42
42
  "prebuild": "rimraf dist",