Skip to content

Commit 68e09e8

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/authz-a5-capability-declaration
# Conflicts: # packages/spec/api-surface.json
2 parents 60d1d40 + 0fcef9b commit 68e09e8

27 files changed

Lines changed: 1816 additions & 138 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
---
4+
5+
feat(platform-objects): sys_user 记录页新增 Permission Sets 与 Business Units 两个一站式分配 tab (A3, #2920)
6+
7+
管理员现在可在单个用户记录页完成三类分配:岗位(Positions,已有)、直接权限集授权(Permission Sets)、业务单元归属(Business Units)。两个新 tab 均为纯 SDUI 的 `record:related_list` + Add picker:
8+
9+
- **Permission Sets** — junction `sys_user_permission_set`(id-keyed,`relationshipField: 'user_id'`),Add picker 绑定 `sys_permission_set`(`linkField: 'permission_set_id'`)。服务端 audience-anchor(D5/D9)与 delegated-admin(D12)门禁的拒绝原因会显示在 Add 对话框。
10+
- **Business Units** — junction `sys_business_unit_member`(id-keyed,`relationshipField: 'user_id'`),Add picker 绑定 `sys_business_unit`(`linkField: 'business_unit_id'`,按显示字段 `name` 标注)。
11+
12+
tab 顺序为 Positions → Permission Sets → Business Units,四语言标签齐全。
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
ADR-0089 D3b: make the `visibility-root-mislayered` lint check bidirectional. `validateVisibilityPredicates` now accepts an optional `{ layer }` option — `'runtime'` (default, unchanged) flags a `data.`-rooted predicate on a `*.view.ts` / `*.page.ts` surface, and `'metadata'` flags a `record.`-rooted predicate on a `*.form.ts` metadata-editing form. Both directions of the ADR's binding-root rule are now covered. Adds the `VisibilityLayer` / `VisibilityOptions` exported types. Fully back-compat: existing single-argument callers keep the runtime behavior.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/plugin-security": minor
3+
---
4+
5+
ADR-0095 D1: tenant isolation is now **Layer 0** — an independent, always-first,
6+
AND-composed filter (`tenant-layer.ts`), no longer a wildcard `tenant_isolation`
7+
RLS policy OR-merged with business RLS. The effective row filter is
8+
`Layer0(tenant) AND Layer1(business RLS)`; the two share no compiler, merge step,
9+
or bypass bit. The superuser bypass now exempts the tenant wall only as a Layer 0
10+
rule (platform-admin posture + object posture permits: private / platform-global
11+
/ better-auth-managed), never via a business-RLS short-circuit.
12+
13+
**BREAKING (multi-org `tenancy.mode = 'multi'` deployments only; `single` mode is
14+
inert and unchanged).** Retiring the OR-merged tenant policy resolves four
15+
behavior deltas, all toward stronger/correcter isolation:
16+
17+
- **(a) Cross-tenant read leak closed.** A permissive business RLS policy (e.g.
18+
`status == 'public'`) no longer OR-widens tenant scope; a foreign-org row it
19+
matched is now invisible.
20+
- **(b) Member by-id writes narrow to owner-only.** The OR-merge silently widened
21+
`owner_only_writes` (`created_by == me`) back to org-wide, so a member could
22+
by-id update/delete *any* record in their org. Writes are now owner-scoped as
23+
authored. **Migration:** if your deployment intentionally relied on members
24+
editing each other's records org-wide, grant an explicit per-object edit
25+
permission set (position-distributed) where that is wanted — the baseline
26+
`member_default` no longer permits it.
27+
- **(c) Global-catalog objects visible to members.** On a `tenancy.enabled:false`
28+
object, members were scoped by a phantom `organization_id` filter (a column
29+
such objects lack); Layer 0 correctly treats them as non-tenant, so the global
30+
catalog is visible.
31+
- **(e) No-active-org writes fail closed.** A write by a principal with no active
32+
organization on a tenant object is now denied (was owner-scoped only).
33+
34+
`tenant_isolation` is retired from the seeded `organization_admin` /
35+
`member_default` / `viewer_readonly` sets; the `_self` / `_org` identity-table
36+
carve-outs and `owner_only_writes/deletes` are unchanged. Customized seeded sets
37+
keep their overlays (ADR-0094). The driver-level `applyTenantScope` seam is
38+
untouched. See ADR-0095 and framework#2936 (the `extractTargetField` `==` blind
39+
spot this exposes, tracked separately).
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): C2-α — extend the `explain` contract to record granularity (#2920)
6+
7+
The access-explanation contract (ADR-0090 D6) now carries the schema for
8+
record-level authorization explanations, so the β-phase engine
9+
(`plugin-security` + `plugin-sharing`) and the Studio/Setup "view as" UI can be
10+
built against a stable wire shape. Contract-only: no engine or UI changes ship
11+
here.
12+
13+
Request side:
14+
- `ExplainRequest.recordId` (optional) — explain one concrete record at row
15+
granularity. Omitted = the pre-C2 object-level question, answered identically
16+
(backward compatible).
17+
18+
Response side (row-level attribution, present only for record-grained requests):
19+
- New `ExplainMatchedRule` — a concrete share / sharing rule / ownership fact /
20+
team / territory / RLS policy / Layer 0 tenant filter that admitted or
21+
excluded the record at a layer, with its access level (`grants`), how it
22+
reached the principal (`via`), the row predicate (`predicate`), and its
23+
`effect` on the record.
24+
- New `ExplainRecordAttribution` — a layer's per-record determination
25+
(`outcome`, effective `rowFilter`, `matchesRecord`, matched `rules`), attached
26+
as the optional `ExplainLayer.record`.
27+
- New top-level `ExplainDecision.record` — the row-level bottom line
28+
(`recordId`, `visible`, `decidedBy`).
29+
30+
Reserved for the ADR-0095 kernel chain (β fills these; optional, backward
31+
compatible):
32+
- New `tenant_isolation` layer id (Layer 0, the always-first tenant wall).
33+
- New `ExplainLayer.kernelTier` (`layer_0_tenant` | `layer_1_business`) so a
34+
consumer can tell the tenant wall from business RLS without hard-coding ids.
35+
- New `AuthzPosture` enum (`PLATFORM_ADMIN` > `TENANT_ADMIN` > `MEMBER` >
36+
`EXTERNAL`) exposed as the optional `ExplainDecision.principal.posture`.
37+
38+
Backward compatibility: every new field is optional or additive; existing
39+
object-level requests and reports parse unchanged. The contract test locks the
40+
new field shapes alongside the existing ones.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
内置行写护栏:`sys_position` / `sys_capability` 的平台/应用托管行不再可被客户管理员删改。
6+
7+
`sys_permission_set` 早有两道门写护栏(`assertPackageManagedWriteGate`)拦截对 package 托管行的写入,但 `sys_position` / `sys_capability` 缺失对应保护——平台/应用发布的系统岗位与能力(provenance 记录在 `managed_by`)可被管理员直接 delete / update 直达驱动,静默破坏应用的授权基线(ADR-0049:provenance 字段存在却无强制 = 正是要补的 enforcement gap)。
8+
9+
新增 **`assertSystemRowWriteGate`**`packages/plugins/plugin-security/src/security-plugin.ts`,data-write hook 接线与 package 门同处),对这两个对象的托管行施加一道无条件的数据层边界:
10+
11+
- **禁止伪造托管来源**:管理员门的 insert / update 载荷(单对象或数组)不得把 `managed_by` 盖成平台/应用值——只有携带 `isSystem` 的平台 seeder / 包发布路径可写;同时封堵 update-to-forge(把自建行改badge成托管行)。
12+
- **拒绝改删托管行**:对 `managed_by` 已是平台/应用值的行,`delete` / `update` / `transfer` / `restore` / `purge` 一律拒绝。与 `sys_permission_set` 不同,这两个对象没有 ADR-0094 overlay write-through,故写护栏必须在此层直接拒绝,而非下放给下游翻译。
13+
- **管理员自建行不受限**`managed_by``user`/∅(sys_position)或 `admin`(sys_capability)的行完全归管理员所有(含委派管理员在自己 subtree 内的自建行)。
14+
15+
护栏 fail-closed 且不依赖调用方授权——持 `modifyAllRecords` 的超管也无法删除平台岗位。两对象的 `managed_by` 词表不同(sys_position:`system`/`config` 托管,`user`/∅ 自建;sys_capability:`platform`/`package` 托管,`admin` 自建),网关按对象分别判定。错误信息仅含业务文案("此岗位/能力由 平台|应用包 提供,不可删除/修改")。
16+
17+
与 delegated-admin 边界不冲突:`GOVERNED_OBJECTS` 本就不含这两个对象,委派管理仍治理 RBAC 链接表而非定义对象。

.github/workflows/ci.yml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ on:
88
branches:
99
- main
1010

11+
# Superseded runs on the same PR/branch waste runners and delay feedback;
12+
# cancel them. Push runs to main group by commit ref as well, so an in-flight
13+
# main run is cancelled only by a newer main push.
14+
concurrency:
15+
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
16+
cancel-in-progress: true
17+
1118
jobs:
1219
filter:
1320
runs-on: ubuntu-latest
@@ -56,6 +63,9 @@ jobs:
5663
steps:
5764
- name: Checkout repository
5865
uses: actions/checkout@v7
66+
with:
67+
# Full history so `turbo --affected` can diff against the PR base.
68+
fetch-depth: 0
5969

6070
- name: Setup Node.js
6171
uses: actions/setup-node@v6
@@ -93,13 +103,29 @@ jobs:
93103
- name: Install dependencies
94104
run: pnpm install --frozen-lockfile
95105

96-
- name: Run all tests
97-
run: pnpm turbo run test
106+
# PRs: only test packages affected by the diff against the PR base.
107+
# spec sits at the root of the dependency graph, so spec-touching PRs
108+
# still run (close to) everything — but the many PRs that don't touch
109+
# spec skip the bulk of the 75-package matrix.
110+
- name: Run affected tests (PR)
111+
if: github.event_name == 'pull_request'
112+
env:
113+
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
114+
run: pnpm turbo run test --affected
115+
116+
# Push to main: full run, but exclude spec's plain test task — the
117+
# coverage step below executes the exact same 250-file spec suite once,
118+
# with coverage. Previously the spec suite ran twice per push.
119+
- name: Run all tests (push)
120+
if: github.event_name == 'push'
121+
run: pnpm turbo run test --filter=!@objectstack/spec
98122

99123
- name: Generate coverage report
124+
if: github.event_name == 'push'
100125
run: pnpm --filter @objectstack/spec test:coverage
101126

102127
- name: Upload coverage reports
128+
if: github.event_name == 'push'
103129
uses: actions/upload-artifact@v7
104130
with:
105131
name: coverage-report

0 commit comments

Comments
 (0)