SharedObjectManager scopes querysets to what a user may see, which
includes other people's public and shared-with-them objects. Several
mutating rule endpoints treated that visibility as permission to write.
Generalise get_owned_object_or_403 into get_shared_object_or_error, which
takes an explicit access level instead of inferring one:
- READ requires the object to be visible; denial is 404 so the response
does not confirm that an id exists.
- EDIT requires ownership; denial is 403, but only after the visibility
check, so 403 never leaks the existence of an invisible object. This
matters for TransactionRuleAction, whose manager is unscoped.
The previous owner_path resolved to a User and discarded the object, so
it could not express visibility at all. via= now points at the governing
SharedObject and is resolved with a plain getattr, so an unresolvable
path raises instead of silently granting access.
is_visible_to/is_editable_by replace is_accessible_by, which was never
called and tested visibility == "shared", a value that does not exist in
Visibility.
Also fixes two further holes in the same module:
- transaction_rule_delete fell through to delete() whenever the caller
was not in shared_with, so any user could delete a public rule. Now
only the owner deletes; a shared user revokes their own access.
- transaction_rule_view was read-only but is now explicitly READ, so
rules shared with a user stay viewable.
The activate/deactivate control is hidden for rules the user does not
own, instead of rendering a button that always fails.
Refs GHSA-83g9-vjqf-2j5q
Add apps.common.functions.get_owned_object_or_403, an ownership-enforcing
variant of get_object_or_404, and apply it across every rules handler that
resolved a TransactionRule / (UpdateOrCreate)TransactionRuleAction from a URL id
without an owner check. Mirrors the check in transaction_rule_edit
(obj.owner and obj.owner != request.user); objects with no owner remain
accessible, preserving existing behaviour. Nested ownership (actions owned via
their parent rule) is handled with owner_path='rule.owner'.
Revoked tokens previously stayed in the list with no way to remove them.
Adds a delete action (hard delete, scoped to the owner, gated behind
demo mode) shown on revoked rows, alongside the existing revoke action on
active ones.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Personal API tokens (model, user-settings UI, admin, management command,
DRF auth class) for non-interactive API access from automations like n8n.
Raw token shown once; only a SHA-256 hash is stored; last_used_at writes
are throttled.
- OAuth2 authorization server via django-oauth-toolkit with authorization
server metadata and optional, off-by-default Dynamic Client Registration
(RFC 7591), so remote OAuth/MCP clients can authenticate and self-register.
- Tests for token auth, DCR gating and the management commands, plus
.env.example and README documentation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>