Repository navigation
fix(objectql): a free-text search resolves only over the fields the caller may query - #25705
Conversation
📓 Docs Drift CheckThis PR changes 1 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin be8b7fc34d39632cf74dc7a043caeee29fdaefbb && git checkout be8b7fc34d39632cf74dc7a043caeee29fdaefbb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3b8ea15c21caad40686ccb811e2b0b4bd4fef9ca e348f156ef284f6acaeb17627f5c350f492fa167 && git checkout -B drift-repro 3b8ea15c21caad40686ccb811e2b0b4bd4fef9ca && git merge --no-ff e348f156ef284f6acaeb17627f5c350f492fa167
node scripts/docs-audit/affected-docs.mjs --json 3b8ea15c21caad40686ccb811e2b0b4bd4fef9ca
|
席位核对REWORK ·
Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
…arch-readable-fields
Contract reviewServed-tier: ① Derived judgmentsThe head moved from ② Semver levelUnchanged: ③ Boundary flagsThe Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 40162354141 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
席位核对Queue red, not this PR's ·
Generated by Claude Code |
Fixes #25672
Clause-②: yes (widening)
Metadata-protocol: none
Declaration
This PR declares an acceptance widening. A member's
GET /api/v1/data/sys_user?search=…was refused with 403 and is now served with 200. On a published REST door, the same input's answer moves from refuse to accept. One search for published text that already denied that refusal found none. ADR-0061 D5 says only that field-level security "applies automatically" to search, which does not settle whether the expansion narrows or refuses. So this PR declares the widening rather than quoting. Every refusal that stays (an explicitfilterorsearchFieldson a hidden field) is unchanged.Metadata-protocol: noneandpatchare kept: no authorable metadata key or schema changes.What
A free-text
searchnow resolves only over the fields the caller may query.expandSearchOnAst(packages/objectql/src/engine.ts) asks the registeredsecurityservice'sgetQueryableFields(object, context)and passes the answer toexpandSearchToFilterasqueryableFields.search-filter.tsthen withholds every resolved field outside that set. The answer is read through the engine's existingSECURITY_SERVICE_SOURCE_SLOT, and the context is the one the middleware will see. This applies tofind,findOne,countandaggregate.getQueryableFieldsis the published complement of what the field-read predicate guard refuses (samecomputeQueryGuardFieldPermsderivation), andsearchAllalready narrows with it. The engine adds no second field-permission rule.searchFields(either spelling) reaches the guard and is refused with 403, as before.searchterm on an object with no searchable field is dropped silently, so the read answers every row (an empty auto-default search set is not refused) #24284 refusal (400 INVALID_FIELD,param: search), with a message that names field-level security as the cause.undefinedanswer leaves the set as it is, and the guard decides as today.Proof (fresh boot,
pnpm dev:crm -- --fresh, a non-admin org member vs the seeded admin)GET /api/v1/data/sys_user?search=Mia?search=admin@objectos(email)populate=primary_business_unit_id&top=25&search=Adm&filter=["banned","!=",true]?filter=["ban_reason","=","x"]PERMISSION_DENIED(guard holds)?search=x&$searchFields=ban_reasonban_reason?search=x&searchFields=name,rolerole?search=userrole: the admin's set is unchanged){ ...expandOptions, queryableFields }with{ ...expandOptions }viascripts/ablation-replace.mjs(anchor 1 → 0, blob7383042fc6ad→cf902b78f292), rebuilt, andablation-dist-preflight --absentpassed. Re-booted, the member's three searches answered 403... not readable by the caller: role, ban_reason, last_login_ip, the card's exact refusal. The admin rows matched the fixed run byte for byte, ids aside.git checkout HEAD --, disk blob7383042fc6ad= HEAD blob,git status --porcelainempty, rebuilt, and the preflight finds the marker indist/core.{js,mjs}.engine-search-queryable-fields.test.ts: an auto-default object loses its hiddenprivate_note). The CRM example has no non-sys_userobject with a member-hidden searchable field to drive over HTTP.Tests (behaviour at
61177d0f12; the head9b04a57b68changes only the changeset declaration line)pnpm --filter @objectstack/objectql exec vitest run: 468/468 files, 8719/8719 tests.pnpm --filter @objectstack/objectql typecheck: exit 0, test layer included.plugin-security: 222 files, 4628 passed, 45 skipped.plugin-audit: 60 files, 985 passed. Both use the rebuilt objectqldist.engine-search-queryable-fields.test.ts(each verb, generality, named-field pass-through, every no-answer road, blank term, all-withheld → 400) and 4 cases insearch-filter.test.ts.Gates
dispatch-gates --commands: 73 derived, all run, all exit 0.--ranreconciliation: 73/73 with exit codes, 0 NOT-MEASURED.check-engine-split-ratio,check-plugin-teardown-shape --self-test) first refused on the shallow clone and passed aftergit fetch --unshallow.eslint --no-inline-configon the four touched files: exit 0.Acceptance notes
packages/platform-objects/src/identity/sys-user.object.tsis untouched: the general route made the narrower one unnecessary, so there is no overlap with feat(plugin-auth): multi-org Create User joins the administrator's current organization #25626.countnarrows with the same set, so the pagedtotalagrees withrecordsfor a member.getQueryableFieldswithholds masked fields too, matching the guard: a member's search no longer matches on a field they see masked, where it used to 403.Generated by Claude Code