Repository navigation
Conversation
Apps register their resource types and permissions, other apps register restriction providers. The app owning a resource asks the permission manager which permissions are left for an actor and enforces the result. Permissions are boolean and every app permission is a kind of one core permission, so restricting a core permission also restricts permissions the restricting app does not know. Access entries allow to restrict only the access gained through one recipient, e.g. a team. Assisted-by: ClaudeCode:claude-fable-5-2 Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Results are kept in a capped cache that restriction providers clear when their restrictions change, so long-running processes neither keep stale results nor grow without limit. A restriction provider that cannot be loaded now restricts everything, like a provider that throws. The permissions that apply to a resource type are read once per check instead of once per resource. Assisted-by: ClaudeCode:claude-fable-5-2 Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Assisted-by: ClaudeCode:claude-fable-5-2 Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
susnux
commented
Nov 12, 2026
| * @experimental 36.0.0 | ||
| */ | ||
| #[Consumable(since: '36.0.0')] | ||
| final readonly class ResourceAccess { |
Contributor
Author
There was a problem hiding this comment.
basically data object pattern here, this makes scoping easier, but we could get rid of this if we just pass e.g. an assoc. array "resource => entries" but I do not really like that idea.
(we need this at least until everything uses unified sharing thus we would only need the resource and the actor, but until then we need resource and potentially multiple entries to let restrictors know about origin)
…rectly `EffectivePermissions::isAllowed()` now also accepts a `CorePermission` case, so callers no longer need `->value` for the core permissions, and the allowed IDs are kept as a map for constant time lookups. Also document why `CorePermission::DOWNLOAD` is a constant and not a case. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
susnux
marked this pull request as ready for review
November 13, 2026 17:33
susnux
requested review from
icewind1991 and
provokateurin
and removed request for
a team
November 13, 2026 17:33
This was referenced Nov 13, 2026
Open
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Apps register their resource types and permissions, other apps register restriction providers. The app owning a resource asks the permission manager which permissions are left for an actor and enforces the result.
Permissions are boolean and every app permission is a kind of one core permission, so restricting a core permission also restricts permissions the restricting app does not know. Access entries allow to restrict only the access gained through one recipient, e.g. a team.
Example usage
Example usage how it can be used directly with this PR:
Details
sequenceDiagram autonumber actor U as Alice (browser) participant T as Tables app participant M as IPermissionManager participant G as Governance (restriction provider) rect rgba(128, 128, 128, 0.12) Note over T,G: Boot, once per process T->>M: registerResourceType(TableResourceType) T->>M: registerResourcePermission(ExportPermission) G->>M: registerResourceRestrictionProvider(LabelRestrictionProvider) end rect rgba(128, 128, 128, 0.12) Note over U,G: Alice opens her list of tables U->>T: GET /tables T->>T: load tables and the shares that give Alice access T->>M: checkAll(Actor of Alice, tables + access entries) M->>G: getRestrictions(actor, all tables) G-->>M: table 7: core:download restricted (label "Internal") M-->>T: EffectivePermissions per table T-->>U: tables with their allowed permissions Note over U: Download is hidden for table 7 end rect rgba(128, 128, 128, 0.12) Note over U,G: Alice calls the download endpoint anyway U->>T: GET /tables/7/download T->>M: check(Actor of Alice, table 7, access entries) Note over M: answered from the cache M-->>T: EffectivePermissions T->>T: isAllowed("core:download") is false T-->>U: 403 Forbidden end rect rgba(128, 128, 128, 0.12) Note over U,G: Later the label is removed from table 7 G->>M: clearCache() U->>T: GET /tables/7/download T->>M: check(Actor of Alice, table 7, access entries) M->>G: getRestrictions(actor, table 7) G-->>M: no restrictions M-->>T: EffectivePermissions T-->>U: 200, the file endIn a follow up we can make it integrated with Interaction API:
Details
sequenceDiagram autonumber actor U as Alice (browser) participant T as Tables app participant I as Interaction API participant L as Core listener (new) participant M as IPermissionManager participant G as Governance U->>T: share table 7 with a receiver T->>I: RestrictInteractionEvent(actor, table 7, action "core:share", receivers) I->>L: dispatch L->>M: check(actor, table 7) M->>G: getRestrictions(actor, table 7) Note over M,G: skipped when the answer is cached G-->>M: restrictions from labels M-->>L: EffectivePermissions alt core:share is restricted L-->>I: throws InteractionRestrictedException I-->>T: restricted, with a hint T-->>U: 403 with the hint Note over G: Governance wrote no listener for this else core:share is allowed L-->>I: no objection I->>G: dispatch to the receiver listener alt receiver breaks a rule, e.g. external receiver on an "Internal" label G-->>I: throws InteractionRestrictedException I-->>T: restricted, with a hint T-->>U: 403 with the hint else receiver is fine G-->>I: no objection I-->>T: allowed T->>T: create the share T-->>U: 201 Created end end Note over I: every outcome is written to the audit log, as todayAnd also a follow up to integrate unified sharing is planned:
Details
sequenceDiagram autonumber actor U as Alice (browser) participant T as Tables app participant M as IPermissionManager participant S as Unified sharing participant G as Governance (provider) participant C as Circles (provider) participant I as Interaction API rect rgba(128, 128, 128, 0.12) Note over U,C: Alice opens table 7. Tables passes only the resource. U->>T: GET /tables/7 T->>M: check(actor, table 7) M->>S: shares on table 7 that apply to Alice? S-->>M: share 42 to team Sales: read, update · share 17 to Alice: read Note over M: shares become access entries (team Sales, user Alice) M->>G: getRestrictions(actor, table 7 + entries) G-->>M: core:download restricted for the resource M->>C: getRestrictions(actor, table 7 + entries) C-->>M: nothing, Alice is a full member of Sales M-->>T: allowed: read, update T-->>U: table 7 with its allowed permissions end rect rgba(128, 128, 128, 0.12) Note over U,I: Alice shares table 7 with Bob. Tables is not involved. U->>S: create share on table 7 for Bob with read, update S->>M: check(actor, table 7) M-->>S: allowed: read, update (from the cache) Note over S: Alice can grant at most what she is allowed herself S->>I: RestrictInteractionEvent(actor, table 7, action "core:share", receiver Bob) Note over I: core listener and receiver listeners, as in the previous diagram I-->>S: allowed S->>S: store the share with permission IDs S-->>U: 201 Created endEntities of this design
Owning app
Mail, Tables, later Files
Core
The new API
Restricting app
Governance, later Circles
Checklist
3. to review, feature component)stable32)AI (if applicable)