Skip to content

๐Ÿ” Broker Access Control (RBAC)

LibreFolio implements a granular Role-Based Access Control (RBAC) system for Brokers. This allows users to share access to their brokerage accounts with other users (e.g., family members, accountants) while maintaining control over permissions.

๐Ÿ“– Overview

Access is managed via the BrokerUserAccess table, which links a User to a Broker with a specific UserRole.

erDiagram
    USER ||--o{ BROKER_USER_ACCESS : "has access"
    BROKER ||--o{ BROKER_USER_ACCESS : "granted to"

    BROKER_USER_ACCESS {
        int user_id FK
        int broker_id FK
        enum role "OWNER, EDITOR, VIEWER"
        decimal share_percentage "0..1, OWNER-only, scales aggregation"
    }

๐Ÿ›ก๏ธ Roles and Permissions

There are three roles with increasing levels of privilege:

Feature VIEWER EDITOR OWNER
View Broker Details โœ… โœ… โœ…
View Transactions โœ… โœ… โœ…
View Reports/Charts โœ… โœ… โœ…
Add/Edit Transactions โŒ โœ… โœ…
Import Files (BRIM) โŒ โœ… โœ…
Edit Broker Settings โŒ โœ… โœ…
Manage Access (Add/Remove Users) โŒ โŒ โœ…
Delete Broker โŒ โŒ โœ…

๐Ÿ“‹ Role Definitions

  1. ๐Ÿ‘๏ธ VIEWER: Read-only access. Ideal for sharing portfolio visibility without risk of data modification.
  2. โœ๏ธ EDITOR: Operational access. Can manage the day-to-day data (transactions, imports) and broker settings (name, icon), but cannot perform destructive administrative actions (deleting the broker) or change who has access.
  3. ๐Ÿ‘‘ OWNER: Administrative access. Full control over the broker.

๐Ÿ“ Key Rules & Constraints

๐Ÿ“Š Ownership share (share_percentage)

Each access row carries a share_percentage (0.000000โ€“1.000000) used only for portfolio aggregation, never for permissions:

  • OWNER defaults to 1.00 and may be reduced for co-ownership (e.g. joint accounts); EDITOR and VIEWER default to 0.00 (delegated operator, read-only accountant).
  • The sum of shares across a broker's users must not exceed 1.00 (validated on add/update); it can be less (a co-owner may simply not be in the system).
  • 0 % is a valid share: a 0 % OWNER keeps full admin rights but contributes nothing to the owner's aggregated numbers.
  • Aggregation scales by share only for OWNERs โ€” EDITOR/VIEWER rows always carry share 0 by schema rule yet see full data, so the engine uses scale 1 for them (effective_share() in financial_math/native_cash.py, called by portfolio_service.py and financial_math/portfolio/engine.py). Role/share edits change every scaled number but not the underlying data, so they also join the cache fingerprint for instant invalidation.

๐Ÿ”’ The "Last Owner" Rule

To prevent brokers from becoming "orphaned" (inaccessible by anyone with admin rights), the system enforces a strict rule:

The last OWNER of a broker cannot be removed or downgraded.

If a broker has only one user with the OWNER role:

  • โŒ That user cannot remove themselves.
  • โŒ That user cannot change their role to EDITOR or VIEWER.
  • โœ… To leave the broker, they must first promote another user to OWNER or delete the broker entirely.

๐Ÿ”ง Self-Management

  • ๐Ÿšช Leaving: any user can remove themselves from a broker at any time (leave_broker). EDITOR/VIEWER always succeed. An OWNER may leave when at least one other OWNER remains โ€” otherwise leaving as the last OWNER deletes the broker entirely (cascade over transactions; BRIM report files are cleaned up by the API layer).
  • โฌ‡๏ธ Self-demotion (update_own_role): EDITOR โ†’ VIEWER is always allowed; OWNER โ†’ EDITOR/VIEWER is allowed only when another OWNER remains, and demoting from OWNER zeroes the share (only OWNERs may hold share > 0). Promotions stay on the OWNER-only path.

๐Ÿ”ง Implementation Details

The logic is centralized in backend/app/services/broker_service.py.

  • ๐Ÿ” _check_user_access(broker_id, user_id, min_role): Core internal method to verify permissions.
  • โž• add_access(): Grants access to a new user (OWNER only).
  • ๐Ÿ”„ update_access(): Changes an existing user's role (OWNER only).
  • โŒ remove_access(): Revokes access (OWNER can remove anyone; others can only remove themselves).
  • ๐Ÿšช leave_broker() / update_own_role(): The self-service paths described above.

๐ŸŒ API Endpoints

Access management is exposed via the following endpoints:

  • GET /api/v1/brokers/{id}/access: List all users with access.
  • POST /api/v1/brokers/{id}/access: Grant access.
  • PATCH /api/v1/brokers/{id}/access/{user_id}: Change role.
  • DELETE /api/v1/brokers/{id}/access/{user_id}: Revoke access.

๐Ÿ‘‘ Superusers and the User CLI

Besides the broker roles, every account carries one system-wide flag, users.is_superuser: the administrator of the admin manual. It belongs to the account, not to a broker.

  • Granted to the first account registered (POST /api/v1/auth/register when count_users() is 0, accepted even when enable_registration is off), to every account created with ./dev.py user create (scripts/user_cli.py calls user_service.create_user() with is_superuser=True), and by ./dev.py user promote.
  • Promote and demote go through user_service.set_user_admin(), which refuses a no-op (already an admin, not an admin) and does not check that another superuser remains. The only last-superuser guard is on self-deletion: DELETE /api/v1/auth/users/me answers 400 when count_superusers() is 1 or less.
  • Applied at the next request: the user CLI writes the database directly, with the server running or not, and get_current_user() reloads the user on every request. A deactivated account (user deactivate, user_service.set_user_active()) gets 401 User account is disabled, and its login 401 Account is disabled.
  • Password reset (user reset, user_service.reset_password()) changes only the hash. The session JWT carries the user id alone (sub, with iat and exp) and nothing revokes it: a token issued before the reset stays valid until it expires (session_ttl_hours). Deactivation is the immediate lock-out.
  • CLI password rules: create and reset call validate_password() in scripts/user_cli.py: at least 8 characters, with an upper-case letter, a lower-case letter, a digit and a symbol from its special_chars set. The API schemas only ask for 8 characters.

The admin commands are in Command-Line Tools.