Skip to content

feat: add API to restrict permissions on resources of other apps - #66546

Open
susnux wants to merge 4 commits into
masterfrom
feat/resource-permissions
Open

susnux wants to merge 4 commits into
masterfrom
feat/resource-permissions

Conversation

@susnux

@susnux susnux commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

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
    end
Loading

In 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 today
Loading

And 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
    end
Loading

Entities of this design

Owning app

Mail, Tables, later Files

  • Declares its permissions and resource types once.
  • Asks what is restricted for an actor on its resources.
  • Enforces the answer, when showing a resource and when an action happens.

Core

The new API

  • Holds the declarations.
  • Asks every restricting app and combines the answers.
  • Applies the "kind of" relation between permissions.

Restricting app

Governance, later Circles

  • Stores its own rules (labels, roles).
  • Answers which permissions it restricts for an actor on a resource.
  • Renders its editor from the declared permissions.
sequenceDiagram
  participant A as Owning app, e.g. Tables
  participant C as Core
  participant G as Governance
  participant R as Circles
  A->>C: actor, resources, optional access entries
  C->>G: what do you restrict?
  G-->>C: download off, label Internal
  C->>R: what do you restrict?
  R-->>C: update and share off for the team Sales entry
  Note over C: restricted by any one = restricted, then apply kind of
  C-->>A: on or off per permission, per resource
  Note over A: enforce when showing the resource and when the action happens
Loading

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

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>
* @experimental 36.0.0
*/
#[Consumable(since: '36.0.0')]
final readonly class ResourceAccess {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@susnux susnux added this to the Nextcloud 36 milestone Nov 12, 2026
…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
susnux marked this pull request as ready for review November 13, 2026 17:33
@susnux
susnux requested a review from a team as a code owner November 13, 2026 17:33
@susnux
susnux requested review from icewind1991 and provokateurin and removed request for a team November 13, 2026 17:33
@susnux susnux added the 3. to review Waiting for reviews label Nov 13, 2026
@susnux
susnux requested a review from salmart-dev November 13, 2026 17:33
@susnux
susnux added this pull request to stack #66613 November 14, 2026 08:27

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants