Account Authorization and Permissions#
Accounts in Sure can be shared between family members at different access levels. The permission system has two layers: database-level scopes on the Account model that filter which accounts a user can see, and controller-level enforcement through the AccountAuthorizable concern that gates write operations on specific permission levels.
Permission Levels#
There are four effective permission levels, resolved by Account#permission_for(user):
| Level | Source | Can view | Can annotate | Can write/manage |
|---|---|---|---|---|
:owner | accounts.owner_id == user.id | ✅ | ✅ | ✅ |
:full_control | account_shares.permission = 'full_control' | ✅ | ✅ | ✅ |
:read_write | account_shares.permission = 'read_write' | ✅ | ✅ | ❌ |
:read_only | account_shares.permission = 'read_only' | ✅ | ❌ | ❌ |
The method returns :owner for the account owner, or the AccountShare#permission value (as a symbol) for shared users . Ownership is non-transferable during normal use; it is only reassigned automatically when a user is purged from the family .
Data Model#
AccountShare (app/models/account_share.rb) is the join table between accounts and non-owner users:
permission— one offull_control,read_write,read_onlyinclude_in_finances— boolean controlling whether the shared account factors into the user's financial calculations- Unique constraint on
(account_id, user_id); cannot share with the account's owner; user must be in the same family
Account convenience helpers :
share_with!(user, permission:, include_in_finances:)— create a shareunshare_with!(user)— remove all shares for a userauto_share_with_family!— bulk-share with all other family members atread_writepermission; called on account creation whenFamily#share_all_by_default?is true
Database-Level Scopes#
Three scopes on Account filter rows at the query level :
accessible_by(user)— owned accounts + any account with a share record . Used everywhere for read access.writable_by(user)— owned + shares withfull_controlonly .included_in_finances_for(user)— owned + shares withinclude_in_finances = true.
Current.accessible_accounts and Current.finance_accounts surface these through the request cycle , delegating to User#accessible_accounts and User#finance_accounts.
Controller-Level Enforcement#
AccountAuthorizable concern#
app/controllers/concerns/account_authorizable.rb is included in ApplicationController and exposes a single private method:
require_account_permission!(account, level = :write, redirect_path: nil)
It resolves the current user's permission on the account and compares against three levels :
level arg | Allowed permissions |
|---|---|
:write (default) | :owner, :full_control |
:annotate | :owner, :full_control, :read_write |
:owner | :owner only |
On failure it redirects (HTML), renders a Turbo Stream redirect, or returns a 403 JSON response, and returns false so callers can return unless require_account_permission!(...) .
Usage patterns#
AccountsController uses a before_action :set_manageable_account for destructive actions (toggle_active, toggle_exclude_from_reports, destroy, unlink, etc.) — it finds the account via accessible_accounts then inline-checks for :owner or :full_control . Read actions like show use set_account, which only calls accessible_accounts.find(params[:id]) with no permission check .
TransactionsController calls require_account_permission! inline per action :
create—:write(default)update_tags—:annotate, with a custom redirect pathmerge_duplicate,unlock,mark_as_recurring, etc. —:write
Family-Level Sharing Defaults#
Family#default_account_sharing is either "shared" or "private" . When "shared", every new account is auto-shared at read_write with all other family members at creation time . Existing families were backfilled to "shared" mode when the feature shipped.