Commit 0902528
committed
fix(workspaces): serialize the workspace.create gate against permission-group writes
The interim fix hoisted the whole capability gate out of the creation
transaction. That removed the second pooled-connection checkout the
`DbTxTripwire` fires on, but left the revocation window open: an admin could
revoke `workspace.create` between the gate and the insert. The original
placement did not close it either — creation took `organization-mutation:<org>`
while permission-group writes take `permission_group:<org>`, so the two never
contended.
Split the decision along the line that actually matters:
- ENTITLEMENT (is this organization governed by permission groups at all)
stays outside the transaction. It bottoms out in `isOrganizationOnEnterprisePlan`,
which is `cache(resolveOrganizationEnterprisePlan)` and admits no options
object by design — an executor argument would miss the memo on every call, and
bypassing the cache resolves a lapsed read to `config: null`, meaning every
capability ALLOWED. `permission_group:<org>` never serialized subscription
changes, so holding it across this read excluded nothing. A concurrent lapse
now applies the group config for one more request (refuses, does not permit);
a concurrent grant skips it for one more request, the same answer the route's
own preflight gave microseconds earlier.
- The CAPABILITY itself is re-read INSIDE the transaction, on the transaction
executor, under `acquirePermissionGroupOrgLock` — the same advisory lock every
permission-group mutation takes. That is what makes the check-to-insert window
closed rather than merely narrow. `resolveDefaultGroup` gains an executor
parameter; nothing else in the chain is threaded.
The pre-transaction capability read is REMOVED rather than kept as a fast-fail:
both callers of `createWorkspace` already refuse on `creationPolicy.canCreate`
(`app/api/workspaces/route.ts`), so it re-read the same value microseconds later
and caught only what the under-lock read now catches definitively. Net effect is
one fewer read on the enterprise create path, not one more.
LOCK ORDER: `organization-mutation:<org>` -> `user-billing-identity:<user>` ->
`<user>:<org>` -> `permission_group:<org>`. The new lock is taken last, and
safely so: it is a leaf. Every transaction that holds it — the five
`organizations/[id]/permission-groups` route transactions — acquires no further
advisory lock afterwards, so no holder can be waiting on any of the three above
it and no cycle can form. It is also taken only AFTER live membership is
confirmed, so a caller who turns out not to belong to the organization never
serializes against its admins, and only when the regime is active, so
non-enterprise and unaffiliated creators take nothing new.
`createDefaultPersonalWorkspaceInTransaction`, which runs inside the enterprise
owner claim's external transaction, passes both organization ids as `null`, so
it is ungoverned by construction, takes no permission-group lock, and cannot
deadlock against the locks that transaction already holds.
`acquirePermissionGroupOrgLock` moves to `lib/permission-groups/locks.ts` so
`lib/workspaces/policy.ts` can acquire it without importing from `app/api/**`;
the five route call sites are repointed rather than re-exported.
Contention this adds, honestly: workspace creates in an Enterprise organization
with Access Control now serialize against that organization's permission-group
admin writes, and against each other, on one org-wide key. Personal creates by
members of such an organization take that lock where they previously took none.
Permission-group writes are low-frequency admin actions and the lock is held for
one indexed single-row read, so the expected wait is negligible — but it is a
real new serialization point, not a free one.1 parent 4fbf1d6 commit 0902528
12 files changed
Lines changed: 538 additions & 168 deletions
File tree
- apps/sim
- app/api/organizations/[id]/permission-groups
- [groupId]
- members
- bulk
- lib
- permission-groups
- workspaces
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | | - | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
17 | | - | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | | - | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | | - | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| |||
Lines changed: 0 additions & 30 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | 44 | | |
75 | 45 | | |
76 | 46 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
1 | 2 | | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
8 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
9 | 13 | | |
10 | 14 | | |
11 | 15 | | |
| |||
90 | 94 | | |
91 | 95 | | |
92 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| |||
90 | 91 | | |
91 | 92 | | |
92 | 93 | | |
93 | | - | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
94 | 102 | | |
95 | | - | |
| 103 | + | |
| 104 | + | |
96 | 105 | | |
97 | | - | |
| 106 | + | |
98 | 107 | | |
99 | 108 | | |
100 | 109 | | |
| |||
281 | 290 | | |
282 | 291 | | |
283 | 292 | | |
284 | | - | |
| 293 | + | |
285 | 294 | | |
286 | 295 | | |
| 296 | + | |
| 297 | + | |
287 | 298 | | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
292 | | - | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
293 | 323 | | |
294 | | - | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
295 | 338 | | |
296 | 339 | | |
0 commit comments