Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 8.1.1

### Added

- `Database::authorizeGrants(string $collection, Document $document, ?callable $resolve = null): void` runs only the
grant check of `authorizeCreate()` and `authorizeUpdate()`, and writes nothing. It refuses a related document given
as a document that adds a permission for a role the caller does not hold, with the same `Exception\Authorization`
(`Permissions must be one of: (<roles>)`), and malformed permissions with the same `Exception\Structure`. Related
documents are first prepared by the `Hook\Nested` hooks, and `$resolve` gives their staged view, as for
`authorizeCreate()`. It checks no create or update permission on the collection, the document or any related
collection or document, so a caller can report an unheld grant before those checks, such as for a staged upsert.
`$document` is the document or the changes a later write would be given. It does nothing, and reads nothing, unless
grants are checked (`setCheckGrants(true)`), authorization is enabled and the relationship hook is attached and
enabled; while they are, a missing collection throws `Exception\NotFound`.

## 8.1.0

8.1.0 adds opt-in and additive APIs for work that callers had to do themselves. With no `Hook\Nested` hook registered
Expand Down
26 changes: 22 additions & 4 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Register it with `$database->addHook($hook)`. Remove it with `removeHook($hook)`
- **What it is given.** Each related document that `createDocument()`, `createDocuments()`, `updateDocument()`,
`upsertDocument()`, `upsertDocuments()`, `authorizeCreate()` or `authorizeUpdate()` is given as a document (a
`Document` or an associative array), at every depth the write relates. `$collection` is the collection the related
document belongs to.
document belongs to. Since 8.1.1, `authorizeGrants()` gives it the same documents, only while it checks grants.
- **When it runs.** Before the write checks the document's grants and relates it, a parent before its children.
Several hooks run in the order they were registered, each given the document the previous one returned.
- **What it returns.** It is given a copy, never the caller's document. The write checks and relates the document it
Expand Down Expand Up @@ -104,9 +104,9 @@ Opt-in. By default 8.1 never compares the permissions a write grants with the ro

With `setCheckGrants(true)`, the check covers related documents written through a parent: every related document
given as a document to `createDocument()`, `createDocuments()`, `updateDocument()`, `upsertDocument()`,
`upsertDocuments()`, `authorizeCreate()` or `authorizeUpdate()`, as deep as the write relates them, each as the
[`Hook\Nested` hooks](#preparing-nested-related-documents) return it. The document the write is given is not checked.
For each related document whose `$permissions` is not null:
`upsertDocuments()`, `authorizeCreate()`, `authorizeUpdate()` or `authorizeGrants()`, as deep as the write relates
them, each as the [`Hook\Nested` hooks](#preparing-nested-related-documents) return it. The document the write is given
is not checked. For each related document whose `$permissions` is not null:

- Malformed permissions throw `Exception\Structure` with the `Validator\Permissions` description.
- The permissions it adds must name roles the caller holds (`Authorization::hasRole()`). Otherwise the write throws
Expand All @@ -133,6 +133,24 @@ A related document's current permissions are read whether or not the caller can
- A related document whose `$id` a `Hook\Nested` hook assigned is new to this check: none of its permissions are read,
locked or resolved, so all of them are checked.

Since 8.1.1, `authorizeGrants(string $collection, Document $document, ?callable $resolve = null): void` runs this check
alone, without writing anything. It judges the related documents of `$document` exactly as `authorizeCreate()` and
`authorizeUpdate()` do, after the `Hook\Nested` hooks and through `$resolve`, and throws the same exceptions, but it
checks no create or update permission. Call it first when an unheld grant must be reported even if the caller also
lacks permission on the parent, for example for a staged upsert that is not yet known to create or update:

```php
$database->authorizeGrants('books', $book, $resolve);
if ($stored->isEmpty()) {
$database->authorizeCreate('books', $book, $resolve);
} else {
$database->authorizeUpdate('books', $stored, $book, $resolve);
}
```

It does nothing, and reads nothing, unless grants are checked, authorization is enabled and the relationship hook is
attached and enabled.

While grants are checked, `updateDocuments()` refuses a related document given as a document with
`Exception\Relationship` (`Invalid relationship value. A bulk update takes related documents by ID while grants are
checked.`), because it is not checked there. Related documents given by ID still link.
Expand Down
4 changes: 2 additions & 2 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -1165,8 +1165,8 @@ public function withPreserveSequence(bool $preserve, callable $callback): mixed

/**
* Set whether, while authorization is enabled, a related document given as a document to createDocument(),
* createDocuments(), updateDocument(), upsertDocument(), upsertDocuments(), authorizeCreate() or authorizeUpdate()
* may only add permissions for roles the caller holds, as deep as the write relates it. Permissions it already
* createDocuments(), updateDocument(), upsertDocument(), upsertDocuments(), authorizeCreate(), authorizeUpdate()
* or authorizeGrants() may only add permissions for roles the caller holds, as deep as the write relates it. Permissions it already
* has may be sent back, and permissions it inherits from its parent are never checked. Malformed permissions
* throw a structure exception and an unheld role an authorization exception, before anything is written.
* While it is on, updateDocuments() refuses a related document given as a document with a relationship
Expand Down
8 changes: 4 additions & 4 deletions src/Database/Hook/Nested.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
* Hook for preparing each nested related document of a write before the write relates it.
*
* It is given each related document that createDocument(), createDocuments(), updateDocument(), upsertDocument(),
* upsertDocuments(), authorizeCreate() or authorizeUpdate() is given as a document (a {@see Document} or an
* associative array), at every depth the write relates. It runs before the write checks the document's grants and
* relates it, a parent before its children. Hooks run in the order they were registered, each given the document
* the previous one returned.
* upsertDocuments(), authorizeCreate(), authorizeUpdate() or, while it checks grants, authorizeGrants() is given as a
* document (a {@see Document} or an associative array), at every depth the write relates. It runs before the write
* checks the document's grants and relates it, a parent before its children. Hooks run in the order they were
* registered, each given the document the previous one returned.
*
* It is given a copy, never the caller's document, and the document it returns is the one the write uses. In
* createDocuments(), upsertDocument() and upsertDocuments() the prepared related documents replace the relationship
Expand Down
32 changes: 32 additions & 0 deletions src/Database/Trait/Documents.php
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,38 @@ public function authorizeUpdate(string $collection, Document $old, Document $doc
}
}

/**
* Check, without writing anything, only the grants of the related documents nested in $document: the part of
* authorizeCreate() and authorizeUpdate() that refuses a related document adding a permission for a role the
* caller does not hold. It checks neither create nor update on $collection, on $document or on any related
* collection or document, so it can run before them, as for a staged upsert that is not known to create or update
* yet. Related documents are first prepared by the registered {@see \Utopia\Database\Hook\Nested} hooks, and
* $resolve gives their staged view, both as for authorizeCreate(). The grants judged are the same whether the
* later write creates or updates the document that relates them.
*
* It does nothing, and reads nothing, unless grants are checked ({@see Database::setCheckGrants()}), authorization
* is enabled, and the relationship hook is attached and enabled.
*
* @param string $collection The collection identifier
* @param Document $document The document, or the changes, a later write would be given; it is not changed
* @param (callable(string, string): ?Document)|null $resolve The staged view of a related document; null judges every one as stored
*
* @throws AuthorizationException When a related document adds a permission for a role the caller does not hold
* @throws NotFoundException When the collection does not exist
* @throws RelationshipException When a related document given as an array is not a valid document
* @throws StructureException When a related document's permissions are malformed
*/
public function authorizeGrants(string $collection, Document $document, ?callable $resolve = null): void
{
if (! $this->checksGrants() || ! $this->relationshipHook?->isEnabled()) {
return;
}

$definition = $this->silent(fn () => $this->getCollection($collection));

$this->prepareNestedCopy($definition, $document, $this->nestedPreparation($resolve));
}

/**
* @param string $collection The collection identifier
* @param Document $document The document to create
Expand Down
Loading
Loading