feat(scope): unattributed and all org targets

Adds a first-class org target vocabulary shared by every rule kind: a
specific organization, unattributed (objects whose org is NULL), or all.
A role can now be granted the unassigned pile without a global rule.

- rules.scope gains 'unattributed' and 'all'
- user_rules.org_id accepts NULL to target unattributed objects
- org_in_scope partitions the classes: 'unattributed' matches only a NULL
  target, tree scopes never match one
- has_permission(user, activity, view) capability probe for UI gating
- current_org_filter() parses morbac.org_ids once into org UUIDs plus the
  unattributed-bucket flag (a JSON null element requests it)
- rls_check split by arity so NULL never carries two meanings:
  rls_check(activity, view) for tables with no org column,
  rls_check(activity, view, row_org_id[, row_user_id]) where a NULL
  row_org_id means the record is unattributed
- detect_rule_conflicts is scope-aware, so rules targeting different
  object sets no longer collide

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 22:35:03 +02:00
parent 6dd1026c5a
commit fa7567f5f0
11 changed files with 630 additions and 72 deletions
+14
View File
@@ -40,5 +40,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Comprehensive test suite with 20 test scenarios
- Complete documentation
- Build and installation automation (Makefile, install.sh)
- Unattributed (no-org) object support:
- Org target vocabulary shared by every rule kind: a specific organization,
`unattributed` (objects with no org), or `all` (every org, unattributed included)
- `rules.scope` values `unattributed` and `all`
- `user_rules.org_id` accepts NULL to target unattributed objects
- `org_in_scope()` partitions the two object classes: `unattributed` matches only
a NULL target, tree scopes never match one
- `morbac.has_permission(user, activity, view)` capability probe for UI gating
- `morbac.current_org_filter()` parses `morbac.org_ids` once into org UUIDs plus
the unattributed-bucket flag (a JSON `null` element requests it)
- `rls_check()` split by arity so NULL never carries two meanings:
`rls_check(activity, view)` for tables with no org column,
`rls_check(activity, view, row_org_id[, row_user_id])` for row-scoped tables
where a NULL `row_org_id` means the record is unattributed
[0.1.0]: https://git.villains.fr/crudy/pgmorbac/releases/tag/v0.1.0