From 0d69ee588a10680947808ab50add6769b5ba4cbd Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Sat, 14 Nov 2026 09:56:34 +0100 Subject: [PATCH] docs(developer): document the access of an actor in the resource permissions API Resource types return the access an actor has to their resources, so a check only needs the resource and can be made by any app. Documents the new meaning of the access entries passed to a check and the access manager. Assisted-by: ClaudeCode:claude-fable-5-2 Signed-off-by: Ferdinand Thiessen --- .../digging_deeper/resource_permissions.rst | 206 ++++++++++++++---- developer_manual/release_notes/new.rst | 1 + 2 files changed, 161 insertions(+), 46 deletions(-) diff --git a/developer_manual/digging_deeper/resource_permissions.rst b/developer_manual/digging_deeper/resource_permissions.rst index d8812b3c0e0..2f92bc6ae67 100644 --- a/developer_manual/digging_deeper/resource_permissions.rst +++ b/developer_manual/digging_deeper/resource_permissions.rst @@ -18,16 +18,18 @@ its resources, without knowing what those resources or permissions are. Three kinds of apps take part: * The **owning app** owns resources, for example tables or mails. It declares - its resource types and permissions, asks which permissions are left for an - actor, and enforces the answer. + its resource types and permissions, tells which access an actor has to its + resources, asks which permissions are left for an actor, and enforces the + answer. * A **restricting app** restricts permissions, for example based on a label attached to a resource or on the role a user has in a team. It never needs code specific to the owning app. -* The **server** holds the declarations, asks every restricting app and - combines the answers. +* The **server** holds the declarations, asks the owning app for the access + of the actor, asks every restricting app and combines the answers. The API only restricts. Whether an actor has access to a resource at all, as -owner or through a share, is decided by the owning app. +owner or through a share, is decided by the owning app. Its resource type +tells the server, so any app can check a resource, not only the app owning it. A check involves all three: @@ -39,8 +41,10 @@ A check involves all three: participant A as Restricting app A participant B as Restricting app B - O->>S: check(actor, resource, access entries) + O->>S: check(actor, resource) activate S + S->>O: getAccess(actor, resources) + O-->>S: access entries S->>A: getRestrictions(actor, accesses) A-->>S: restricted permission IDs S->>B: getRestrictions(actor, accesses) @@ -122,7 +126,8 @@ resource type (``appid:type``), the ID of the resource within that type and, optionally, its owner as an ``\OCP\Actor\Principal``. Only the owning app knows what the ID refers to. -A resource type lists the permissions that apply to its resources. An app can +A resource type lists the permissions that apply to its resources and tells +which access an actor has to them. An app can register several types, for example one for tables and one for rows. There is no hierarchy between resources: a restriction on a table does not apply to its rows unless the restricting app returns it for the rows as well. @@ -150,10 +155,11 @@ Some restrictions only apply to access gained in a certain way. A user who is a visitor in a team should only be able to read what is shared with that team, but keeps full access to a resource that is also shared with them directly. -To make this possible, the owning app can pass the shares that give the actor -access, as it stores them. Each ``\NCU\Resource\Permission\AccessEntry`` names -the recipient of the share and the permissions it grants. The recipient is a -kind and an ID: +To make this possible, the owning app describes every way the actor has +access to a resource as an ``\NCU\Resource\Permission\AccessEntry``, as it +stores it. An entry names the recipient of the access, for example of a share, +and the permissions it grants. Without an entry the actor has no access. The +recipient is a kind and an ID: .. list-table:: :header-rows: 1 @@ -170,8 +176,10 @@ kind and an ID: * - ``appid:kind`` - Defined by your app, for example a link token -You do not need to resolve group or team memberships or roles. Pass the entry -as it is stored; the restricting app knows what to do with it. +You do not need to resolve group or team memberships or roles. Return the entry +as it is stored; the restricting app knows what to do with it. The owner of a +resource gets an entry as well, with the user as recipient and all permissions +of the resource. For owning apps --------------- @@ -221,7 +229,8 @@ Registering resource types ^^^^^^^^^^^^^^^^^^^^^^^^^^ Implement ``\NCU\Resource\IResourceType`` for every type of resource your app -owns and list the permissions that apply to it. +owns. List the permissions that apply to it and return the access an actor has +to resources of the type. .. code-block:: php @@ -232,7 +241,12 @@ owns and list the permissions that apply to it. namespace OCA\Tables\Resource; use NCU\Resource\IResourceType; + use NCU\Resource\Permission\AccessEntry; use NCU\Resource\Permission\CorePermission; + use OCA\Tables\Db\ShareMapper; + use OCA\Tables\Db\TableMapper; + use OCP\Actor\Actor; + use OCP\Actor\PrincipalType; use OCP\IL10N; class TableResourceType implements IResourceType { @@ -240,6 +254,8 @@ owns and list the permissions that apply to it. public function __construct( private IL10N $l10n, + private ShareMapper $shareMapper, + private TableMapper $tableMapper, ) { } @@ -260,8 +276,61 @@ owns and list the permissions that apply to it. ExportPermission::ID, ]; } + + public function getAccess(Actor $actor, array $resources): array { + if ($actor->principal->type !== PrincipalType::User) { + return []; + } + + $userId = $actor->principal->id; + $tableIds = array_map(static fn ($resource) => $resource->id, $resources); + + $access = []; + // One query for all tables + foreach ($this->tableMapper->findOwnedBy($tableIds, $userId) as $table) { + $key = self::ID . '/' . $table->getId(); + $access[$key][] = new AccessEntry( + AccessEntry::RECIPIENT_USER, + $userId, + $this->getPermissionIds(), + ); + } + + foreach ($this->shareMapper->findSharesFor($tableIds, $userId) as $share) { + $key = self::ID . '/' . $share->getTableId(); + $access[$key][] = new AccessEntry( + $share->getReceiverType() === 'circle' + ? AccessEntry::RECIPIENT_TEAM + : AccessEntry::RECIPIENT_USER, + $share->getReceiver(), + $this->toPermissionIds($share), + ); + } + + return $access; + } } +``getAccess`` receives all resources of your type that are part of one check. +Return the entries keyed by ``ResourceReference::key()`` and leave out the +resources the actor has no access to. Things to know: + +* Return an entry for every way the actor has access, including ownership. + Restrictions that only apply to one recipient, such as a team role, can only + take effect on the entry of that recipient. +* Look the owner up in your own storage. ``ResourceReference::$owner`` is + filled in by the caller, which can be another app. +* Load your data for the whole batch at once where possible, a listing can + contain hundreds of resources. +* Permissions in an entry that do not apply to the resource type are ignored + and logged. +* If the method throws an exception, the actor is treated as having no access + to any of the resources and the error is logged. +* The entries are remembered while the process runs. Call + ``IPermissionManager::clearCache()`` whenever the access to a resource + changes, for example when a share is created or removed, so that later + checks in the same process see the change. + Register both in the ``register`` method of your ``Application`` class: .. code-block:: php @@ -278,7 +347,8 @@ Checking a resource ^^^^^^^^^^^^^^^^^^^ Ask ``\NCU\Resource\Permission\IPermissionManager`` which permissions are left -for the actor. The simplest form passes only the resource: +for the actor. Pass only the resource and the server asks your resource type +for the access of the actor: .. code-block:: php @@ -295,39 +365,36 @@ for the actor. The simplest form passes only the resource: $permissions = $this->permissionManager->check($actor, $resource); if (!$permissions->isAllowed(CorePermission::Update)) { - // Not allowed by another app + // No access, not granted or not allowed by another app } -Without access entries the result starts from all permissions that apply to -the resource type. It tells you what other apps restrict, not what the actor -may do: intersect it with the access your app grants. +The result is what is left of the permissions granted by the access entries, +so it can be used directly. If the actor has no access to the resource, no +permission is allowed. -With access entries the result is what is left of the permissions granted by -the entries, so it can be used directly: +If your app has already loaded the access, for example the shares of the +tables it is about to list, pass the entries as third argument. The resource +type is then not asked: .. code-block:: php - use NCU\Resource\Permission\AccessEntry; - - $entries = []; - foreach ($this->shareMapper->findSharesFor($table, $userId) as $share) { - $entries[] = new AccessEntry( - $share->getReceiverType() === 'circle' - ? AccessEntry::RECIPIENT_TEAM - : AccessEntry::RECIPIENT_USER, - $share->getReceiver(), - $this->toPermissionIds($share), - ); - } - $permissions = $this->permissionManager->check($actor, $resource, $entries); -Pass an entry for every way the actor has access, including ownership if your -app treats the owner like a recipient. Restrictions that only apply to one -recipient, such as a team role, can only take effect if you pass entries. +.. list-table:: + :header-rows: 1 + :widths: 30 70 -Permissions in an entry that do not apply to the resource type are ignored -and logged. + * - Entries + - Result + * - Not passed or ``null`` + - The resource type is asked for the access of the actor + * - A list of entries + - The entries are used as they are + * - An empty list + - The actor has no access, no permission is allowed + +Entries you pass have to match what your resource type returns. Other apps +checking the same resource get the answer of the resource type. Checking many resources ^^^^^^^^^^^^^^^^^^^^^^^ @@ -353,6 +420,10 @@ actor is the same for all of them. The result is keyed by ->isAllowed(CorePermission::Delete); } +A ``ResourceAccess`` takes the access entries as optional second argument, +with the same meaning as for ``check``. Every resource type is asked once for +all its resources that were passed without entries. + ``check`` and ``checkAll`` throw an ``InvalidArgumentException`` if a resource type is not registered. ``checkAll`` also throws if the same resource is passed more than once. @@ -367,12 +438,51 @@ The server does not enforce anything for you. Use the result in both places: * when an action happens, to refuse it on the server side. Results are remembered while the process runs, so checking again when the -action happens is cheap. +action happens is cheap. Call ``IPermissionManager::clearCache()`` whenever the +access to a resource changes. Rules that depend on who receives something, such as "must not be shared with external recipients", are not covered by this API. They are handled by the ``\OCP\Interaction\RestrictInteractionEvent``. +For other apps +-------------- + +An app can check resources it does not own, for example to only act on what +the actor may do. Build the reference from the ID of the resource type and the +ID of the resource and call ``check`` or ``checkAll`` without access entries: + +.. code-block:: php + + $resource = new ResourceReference('tables:table', $tableId); + if (!$this->permissionManager->check($actor, $resource)->isAllowed(CorePermission::Read)) { + // The actor has no access to the table or reading is restricted + } + +``IPermissionManager`` tells what is left after all restrictions. To get the +access the actor was given, before any restriction, ask +``\NCU\Resource\Permission\IAccessManager``: + +.. code-block:: php + + $access = $this->accessManager->getAccess($actor, [$resource]); + $entries = $access[$resource->key()] ?? []; + +The result is keyed by ``ResourceReference::key()``, resources the actor has no +access to are left out. A restricting app needs this for its own rules, such as +who may change a restriction: a rule based on the permissions that are left +could prevent lifting the restriction that removed them. + +Always ask the access manager, not the resource type directly. The resource +type is not the only source of access. + +.. mermaid:: + + flowchart LR + types["Resource types
getAccess()"] --> access["IAccessManager
what was granted"] + access --> permissions["IPermissionManager
what is left"] + providers["Restriction providers
getRestrictions()"] --> permissions + For restricting apps -------------------- @@ -425,6 +535,8 @@ Things to know: * The provider is called for resources of every type. Return nothing for resources you have no rules for. +* The provider is only called for resources the actor has access to. The + access entries of every ``ResourceAccess`` are set. * A restriction is a list of permission IDs. Restricting a core permission also restricts every permission that is a kind of it. Unknown IDs are ignored. @@ -444,7 +556,7 @@ Restricting one recipient ``Restriction::forResource`` restricts permissions however the actor has access. ``Restriction::forRecipient`` restricts only the access the actor has -through one recipient, which the owning app passed as an access entry: +through one recipient, that is through one of the access entries: .. code-block:: php @@ -496,10 +608,11 @@ How results are combined For every resource the server -#. takes the permissions that apply to the resource type, -#. without access entries starts from all of them; with access entries takes, - for each entry, the granted permissions without those restricted for the - recipient of the entry, and merges the entries, +#. takes the access entries that were passed, or asks the resource type for + them; without entries no permission is allowed, +#. takes, for each entry, the granted permissions that apply to the resource + type without those restricted for the recipient of the entry, and merges + the entries, #. removes the permissions restricted for the resource as a whole. A permission restricted by one restricting app is restricted, no matter what @@ -530,4 +643,5 @@ actor can do through the team, another one restricts sharing the resource. If a restricting app is disabled, its restrictions no longer apply. If a restriction provider throws an exception during a check or cannot be loaded, every permission on all resources of that check is restricted and the error -is logged. +is logged. If a resource type throws an exception when asked for the access, +no permission is allowed on its resources of that check. diff --git a/developer_manual/release_notes/new.rst b/developer_manual/release_notes/new.rst index 31eb30f5681..3b116705c94 100644 --- a/developer_manual/release_notes/new.rst +++ b/developer_manual/release_notes/new.rst @@ -51,6 +51,7 @@ Resource permissions API The new experimental ``\NCU\Resource`` namespace lets an app restrict what another app allows on its resources, without code specific to that app. Apps that own resources declare their resource types and permissions, +tell which access an actor has to their resources, ask ``\NCU\Resource\Permission\IPermissionManager`` which permissions are left for an ``\OCP\Actor\Actor``, and enforce the result. Apps that restrict permissions, for example based on a label or a team role,