The update check shared the file-based default cache with cachalot, whose culling randomly deleted the key as pages loaded. Store only the latest release in a dedicated cache, compare it to APP_VERSION at render time, and drop the startup delete. Hide the banner when CHECK_FOR_UPDATES is off or APP_VERSION isn't a valid version.
- Add flag-off login page and admin password tests so the refusal tests can't pass for unrelated reasons
- Expect exactly 401 from refused basic auth
- Gate OIDC_DISABLE_LOCAL_LOGIN on an explicit OIDC_CONFIGURED flag
- Realign the README OIDC table, use "email" consistently, and warn about lockout with OIDC_ALLOW_SIGNUP=false
Transfers are still two transactions, now linked to each other:
- shown as one item in lists, with each side's amount, account,
category and tags ("Combine transfers" user setting, on by default)
- date, reference date, paid and mute stay in sync between sides
- edit opens the transfer form; delete, restore, duplicate and bulk
actions act on both sides
- transfers made by the transfer form now trigger rules for each side
Recurring and quick transactions gain a Transfer type, with a "To
account" and optional "To amount", generating linked transfers. A
transfer can be converted into either.
Existing transfers are left as they are, since there is no reliable
way to pair them.
Replace the hard-dated demo fixture with a load_demo_data command that
builds the dataset around the current date, so the demo always has data
in the current month. It also covers more features: multi-currency
accounts with exchange rate history, an ETF and Bitcoin tracked with DCA
strategies, transfers, a credit card using reference dates, installment
plans, recurring transactions, quick transactions, rules and filter
presets.
Credit cards often have paid transactions dated after the invoice being
closed, which made reconciliation against all paid transactions wrong.
The reconciliation panel now lets you balance against all paid
transactions (previous behavior), paid transactions up to a date, or
paid transactions up to a reference month. The adjustment transaction is
dated at the cutoff so later reconciliations with the same or a later
cutoff include it. An invalid cutoff blocks the reconciliation instead
of silently falling back.
ImportProfile and ImportRun had no owner, and neither the web views nor the
REST viewsets filtered by the requesting user, so any authenticated user could
read, edit and delete every other user's import profiles and runs
(GHSA-q668-h56g-mfp4).
- ImportProfile is now a SharedObject, with share and take-ownership views
like transaction rules; the global unique constraint on name is dropped.
- ImportRun is now an OwnedObject owned by the uploader, so runs made with a
shared profile stay private to whoever ran them.
- Existing runs are backfilled with the owner of their imported
transactions; existing profiles stay unowned and usable by everyone.
Moving the pointer diagonally from a submenu trigger to its flyout crosses
the items below it, which closed the flyout before it could be reached.
Delay hiding the flyout by 300ms, and show it immediately on hover or focus.
Parsing helpers now return (value, error) instead of raising, so no
exception object flows into the response while clients keep the
precise messages they had before.
full_clean() failures map to fixed per-field messages; redirect URI
errors surface as NON_FIELD_ERRORS from Application.clean().
Also reject non-string token_endpoint_auth_method with a 400 instead
of crashing with a TypeError.
Allow verified-only linking with OIDC_TRUST_EMAIL=false while retaining case-insensitive matching. Keep unconditional trust enabled by default for compatibility with #518; that mode retains the documented takeover risk.
Categories, Tags and Entities Overview are merged into one Overview page.
Levels are chips that can be dragged (or moved with the arrow keys) to set
the hierarchy and clicked to switch off, so any ordering of the three is
reachable from the same page.
Only the results reload on change; the controls stay in place. The chip
order is kept in the session and defaults to categories by tags, matching
the old Categories Overview.
The rules endpoints were one instance of a pattern repeated across every
SharedObject-backed app. Route the rest through the same helper.
DCA entries were the worst case and are strictly wider than the reported
rules bug: DCAEntry has an unscoped default manager, so filtering by
strategy__id alone reached entries on strategies the caller could not see
at all. No public or shared strategy was needed -- only a guessable
integer. strategy_entry_add/edit/delete now resolve through the parent
strategy with via="strategy".
Every SharedObject delete view carried an inverted condition:
if obj.owner != request.user and request.user in obj.shared_with.all():
obj.shared_with.remove(request.user)
else:
obj.delete()
An object its owner had made public matched neither branch's intent and
fell through to delete(), so any authenticated user could destroy it.
Confirmed reachable for accounts, account groups, categories, tags,
entities and DCA strategies. The owner now deletes, a shared user revokes
only their own access, and anyone else gets a 403.
The API viewsets had no object-level check at all. DjangoModelPermissions
gated them shut for ordinary users, who hold no model permissions, so this
was not reachable in a default install -- but the check belongs there
regardless, and a user granted change_account in the admin could write any
visible account. SharedObjectPermission adds it for the SharedObject
viewsets, leaving reads to SharedObjectManager.
account_toggle_untracked only flips the calling user's own row in the
untracked_by m2m, so it takes READ rather than EDIT.
Refs GHSA-83g9-vjqf-2j5q
Regression tests for GHSA-83g9-vjqf-2j5q, one per endpoint the advisory
named plus the delete and view paths found alongside them: a non-owner
gets 403 on every mutation of a public or shared rule, and the object is
asserted unchanged afterwards.
Also covers the parts that are easy to regress in the other direction:
shared users keep read access, a shared user deleting only revokes their
own access, unowned rules stay claimable, and children of invisible rules
answer 404 rather than 403.
SharedObjectPredicateParityTests asserts is_visible_to agrees with
SharedObjectManager across every owner/visibility/shared combination.
The manager builds a Q and the predicate tests an instance, so they
cannot share an implementation and can otherwise drift apart.
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