Skip to content

fix(sharing,driver-sql): a rule authored in Setup actually applies — and switching it off actually withdraws access - #3850

Merged
os-zhuang merged 2 commits into
mainfrom
claude/sharing-rules-i18n-recipient-02a623
Jul 28, 2026
Merged

fix(sharing,driver-sql): a rule authored in Setup actually applies — and switching it off actually withdraws access#3850
os-zhuang merged 2 commits into
mainfrom
claude/sharing-rules-i18n-recipient-02a623

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Closes #3821 (server-side half). UI half: objectstack-ai/objectui#2920.

What was broken

A rule authored in Setup did nothing to the records already there. Writing a sys_sharing_rule rebound the per-record hooks, which only makes the rule reach records written from then on. An admin who created a rule and switched it on watched the recipient's list stay empty until somebody happened to touch each record.

Switching a rule off did not withdraw access. Deactivating — or deleting — left every grant it had already issued in place, and boot backfill only reconciles ACTIVE rules, so those grants outlived restarts while the UI displayed the rule as disabled. The reconcile existed, but only behind POST /sharing/rules/:id/evaluate, which the Console never calls and offers no button for.

An unsortable query lost its rows, not just its order. SqlDriver.find() already recovered from a SELECT projection naming a missing column; the identical failure one clause over — an unknown ORDER BY column — fell through to return []. Because count() is a separate statement, the list endpoint answered 200 with records: [] and total: 3: the rows are there, none are shown, nothing is logged. Same family as the $-param footgun closed by #2926.

The fixes

  • Every non-system write to sys_sharing_rule now reconciles that rule's grants, chained behind the existing rebind. Insert/update run the same diff-based evaluateRule the REST endpoint runs (it purges when the rule is inactive); delete purges via the new SharingRuleService.revokeRuleGrants, because evaluateRule throws RULE_NOT_FOUND once the row is gone — which is also why a rule deleted through the plain data API used to orphan its grants. Seeding / package bootstrap write with isSystem and are skipped (kernel:bootstrapped backfills those). Best-effort: a failed reconcile logs and never fails the authoring write.
  • Rows outrank their order. The unknown-column retry ladder drops the projection first (likelier culprit, cheaper to lose), then the sort, then gives up. An unsortable query returns its rows unordered instead of empty. Errors that aren't about an unknown column still propagate.
  • zh-CN / ja-JP / es-ES sys_sharing_rule help text refreshed — it still described recipient_type in terms of department (the enum value is business_unit) and told admins to enter a queue name for recipient_id (queue was removed in ADR-0078); es-ES rendered position / unit_and_subordinates as "rol".

Verification

Browser-driven against examples/app-showcase — all data, accounts and rules created through the Console UI:

  • Enabling a rule now grants the recipient every matching record immediately (before: only records that happened to be touched afterwards)
  • Switching it off takes them all away (before: still visible, and still visible after a restart)
  • Criteria authored in the visual builder is honored: {"pinned":true} on an owner-private object granted exactly the matching records
  • Access levels: read → readable, write refused; edit → writable and saves
  • Driver: $orderby=nosuchfield asc now returns 3 rows instead of 0

Tests: plugin-sharing 108 ✓ (7 new reconcile regressions), driver-sql 407 ✓ (6 new recovery regressions). Both new suites verified to fail without their fix.

Known gap, not fixed here

access_level: 'full' is declared but not distinct — the spec comment says "Full Access (Transfer, Share, Delete)", but sharing-service.ts only ever tests { $in: ['edit','full'] }, so 完全访问 behaves exactly like 编辑. Worth its own issue; implementing delete/transfer/share semantics is a feature decision, not a bug fix.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 12:04pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/plugin-sharing.

11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-sharing)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-sharing)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-sql, @objectstack/plugin-sharing)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/security.mdx (via packages/plugins/plugin-sharing)
  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-sql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@baozhoutao

Copy link
Copy Markdown
Contributor Author

The access_level: 'full' gap noted above is now tracked as #3865 — it is not fixed here.

…and switching it off actually withdraws access (#3821)

**Enabling a rule did nothing to the records already there.** Writing a
`sys_sharing_rule` rebound the per-record hooks, which only makes the rule reach
records written FROM THEN ON. An admin who created a rule and switched it on saw
the recipient's list stay empty until somebody happened to touch each record.

**Switching a rule off did not withdraw access.** Deactivating — or deleting —
left every grant it had already issued in place, and boot backfill only
reconciles ACTIVE rules, so those grants outlived restarts while the UI showed
the rule as disabled. The reconcile existed but was reachable only through
`POST /sharing/rules/:id/evaluate`, which the Console never calls and exposes no
button for.

Each non-system write to `sys_sharing_rule` now also reconciles that rule's
grants, chained behind the existing rebind: insert/update run the same
diff-based `evaluateRule` the REST endpoint runs (it purges when the rule is
inactive), and delete purges directly via the new
`SharingRuleService.revokeRuleGrants` — `evaluateRule` can't help there because
the row is gone (`RULE_NOT_FOUND`), which is also why a rule deleted through the
plain data API used to orphan its grants. Seeding and package bootstrap write
with `isSystem` and are skipped; `kernel:bootstrapped` already backfills those.
Reconciliation is best-effort and never fails the authoring write.

**An unsortable query lost its rows, not just its order.** `SqlDriver.find()`
already recovered from a SELECT projection naming a missing column; the same
failure one clause over — an unknown ORDER BY column — fell through to
`return []`. Because `count()` is a separate statement, the endpoint answered
200 with `records: []` and `total: 3`: the rows are there, none are shown,
nothing is logged. Same family as the `$`-param footgun closed by #2926. It
surfaced through the sharing-rule recipient picker, whose client mangled
`'name asc'` into a list of one-character column names (fixed in objectui).
Rows now outrank their order: the retry ladder drops the projection first, then
the sort, then gives up — an unsortable query returns rows UNORDERED instead of
empty. Non-column errors still propagate.

Also refreshes the `sys_sharing_rule` help text in the zh-CN / ja-JP / es-ES
bundles, which still described `recipient_type` in terms of `department` (the
enum value is `business_unit`) and told admins to enter a queue name for
`recipient_id` (`queue` was removed in ADR-0078). The es-ES option labels for
`position` / `unit_and_subordinates` were translated as "rol".

Browser-verified against the showcase Console: enabling a rule immediately gave
the recipient every matching record, and switching it off took them all away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@os-zhuang
os-zhuang merged commit 647ec8b into main Jul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/sharing-rules-i18n-recipient-02a623 branch July 28, 2026 14:16
…s it (#3821)

The field `description`s on `sys_sharing_rule` render as help text under each
input in Setup. They were written as notes for the next engineer:

  接收访问权限的主体类型——求值时会展开为用户授权。`business_unit` 会沿
  parent_business_unit_id 树展开;`team` 为扁平结构(better-auth);`position`
  展开为该岗位的任职人(岗位为扁平结构,ADR-0090 D3)…(ADR-0057 D5)。

ADR numbers, table and column names, a third-party library, and enum machine
values the dropdown never displays — it shows 用户 / 团队 / 业务单元 / 岗位.

Several were also stale, some of them because of this very issue's UI work:
`recipient_id` still said "fill in a business-unit id / team id / position name"
after it became a record picker; `object_name` still said "short object name
(e.g. opportunity, account)" after it became an object picker; `criteria_json`
still described hand-written `FilterCondition` JSON after it became a visual
builder — and carried "(FilterCondition JSON)" in its LABEL.

All of it rewritten for the reader who actually sees it, in en / zh-CN / ja-JP /
es-ES. The engine detail was already in the object's doc comment, which is where
it stays; a comment on the first field now says so, so the next author doesn't
put it back. `active` can finally state what it does — turning it off withdraws
the access — which only became true with the reconcile fix in this branch. The
list page's subtitle and the `managed_by` / `customized` help got the same
treatment.

Browser-verified in the showcase Console: no ADR, table, column, enum or library
name survives anywhere in the create dialog, the detail page or the list header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项

2 participants