Fix RBAC role-based permissions for accounts endpoint

Fixed critical bugs preventing users from seeing accounts through their assigned roles:

1. **Fixed duplicate function definition** (crud.py)
   - Removed duplicate auto_assign_default_role() that only took 1 parameter
   - Kept correct version with proper signature and logging
   - Added get_all_user_roles() helper function

2. **Added role-based permissions to accounts endpoint** (views_api.py)
   - Previously only checked direct user permissions
   - Now retrieves and combines both direct AND role permissions
   - Auto-assigns default role to new users on first access

3. **Fixed permission inheritance logic** (views_api.py)
   - Inheritance check now uses combined permissions (direct + role)
   - Previously only checked direct user permissions for parents
   - Users can now inherit access to child accounts via role permissions

Changes enable proper RBAC functionality:
- Users with "Employee" role (or any role) now see permitted accounts
- Permission inheritance works correctly with role-based permissions
- Auto-assignment of default role on first Castle access

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
padreug 2025-11-12 03:00:17 +01:00
parent c086916be8
commit 52c6c3f8f1
2 changed files with 134 additions and 35 deletions

View file

@ -45,6 +45,7 @@ from .models import (
AccountType,
AccountWithPermissions,
AssertionStatus,
AssignUserRole,
BalanceAssertion,
BulkGrantPermission,
BulkGrantResult,
@ -55,6 +56,8 @@ from .models import (
CreateEntryLine,
CreateJournalEntry,
CreateManualPaymentRequest,
CreateRole,
CreateRolePermission,
CreateUserEquityStatus,
ExpenseEntry,
GeneratePaymentInvoice,
@ -66,11 +69,17 @@ from .models import (
ReceivableEntry,
RecordPayment,
RevenueEntry,
Role,
RolePermission,
RoleWithPermissions,
SettleReceivable,
UpdateRole,
UserBalance,
UserEquityStatus,
UserInfo,
UserRole,
UserWalletSettings,
UserWithRoles,
)
from .services import get_settings, get_user_wallet, update_settings, update_user_wallet
@ -141,12 +150,19 @@ async def api_get_accounts(
- Returns AccountWithPermissions objects when filter_by_user=true, otherwise Account objects
"""
from lnbits.settings import settings as lnbits_settings
from . import crud
all_accounts = await get_all_accounts()
user_id = wallet.wallet.user
is_super_user = user_id == lnbits_settings.super_user
# Auto-assign default role if user has no roles (only for non-super users)
if not is_super_user:
assigned_role = await crud.auto_assign_default_role(user_id, "system")
if assigned_role:
logger.info(f"[ACCOUNTS] Auto-assigned role to user {user_id}")
# Super users bypass permission filtering - they see everything
if not filter_by_user or is_super_user:
# Filter out virtual accounts if requested (default behavior for user views)
@ -157,28 +173,52 @@ async def api_get_accounts(
# Filter by user permissions
# NOTE: Do NOT filter out virtual accounts yet - they're needed for inheritance logic
# Get direct user permissions
user_permissions = await get_user_permissions(user_id)
# Get role-based permissions
role_permissions_list = await crud.get_user_permissions_from_roles(user_id)
# Flatten role permissions into a single list
role_perms = []
for role, perms in role_permissions_list:
role_perms.extend(perms)
# Combine direct and role-based permissions
all_permissions = list(user_permissions) + role_perms
logger.info(f"[ACCOUNTS] User {user_id} has {len(user_permissions)} direct permissions and {len(role_perms)} role permissions (total: {len(all_permissions)})")
if role_perms:
logger.info(f"[ACCOUNTS] Role permissions: {[(p.account_id, p.permission_type) for p in role_perms]}")
logger.info(f"[ACCOUNTS] Total accounts in system: {len(all_accounts)}")
if len(all_accounts) > 0:
logger.info(f"[ACCOUNTS] Sample account IDs: {[acc.id for acc in all_accounts[:5]]}")
# Get set of account IDs the user has any permission on
permitted_account_ids = {perm.account_id for perm in user_permissions}
permitted_account_ids = {perm.account_id for perm in all_permissions}
# Build list of accounts with permission metadata
accounts_with_permissions = []
for account in all_accounts:
# Check if user has direct permission on this account
# Check if user has permission on this account (direct or from role)
account_perms = [
perm for perm in user_permissions if perm.account_id == account.id
perm for perm in all_permissions if perm.account_id == account.id
]
# Check if user has inherited permission from parent account (any permission type)
# Try each permission type to see if user has inherited access
# Check if user has inherited permission from parent account (using combined permissions)
# Check both direct and role-based permissions for parent accounts
inherited_perms = []
for perm_type in [PermissionType.READ, PermissionType.SUBMIT_EXPENSE, PermissionType.MANAGE]:
perms = await get_user_permissions_with_inheritance(
user_id, account.name, perm_type
)
inherited_perms.extend(perms)
for perm in all_permissions:
# Get the account for this permission
perm_account = await get_account(perm.account_id)
if not perm_account:
continue
# Check if this permission's account is a parent of the current account
# e.g., "Expenses:Supplies" is parent of "Expenses:Supplies:Food"
if account.name.startswith(perm_account.name + ":"):
# Inherited permission from parent account
inherited_perms.append((perm, perm_account.name))
# Determine if account should be included
has_access = bool(account_perms) or bool(inherited_perms)
@ -228,6 +268,7 @@ async def api_get_accounts(
acc for acc in accounts_with_permissions if not acc.is_virtual
]
logger.info(f"[ACCOUNTS] Returning {len(accounts_with_permissions)} accounts for user {user_id}")
return accounts_with_permissions
@ -3510,6 +3551,29 @@ async def api_revoke_user_role(
return {"success": True, "message": "Role assignment revoked"}
@castle_api_router.get("/api/v1/admin/users/roles")
async def api_get_all_user_roles(
wallet: WalletTypeInfo = Depends(require_admin_key),
):
"""Get all user role assignments (admin only)"""
from . import crud
user_roles = await crud.get_all_user_roles()
return [
{
"id": ur.id,
"user_id": ur.user_id,
"role_id": ur.role_id,
"granted_by": ur.granted_by,
"granted_at": ur.granted_at.isoformat(),
"expires_at": ur.expires_at.isoformat() if ur.expires_at else None,
"notes": ur.notes,
}
for ur in user_roles
]
@castle_api_router.get("/api/v1/users/me/roles")
async def api_get_my_roles(
wallet: WalletTypeInfo = Depends(require_invoice_key),