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:
parent
c086916be8
commit
52c6c3f8f1
2 changed files with 134 additions and 35 deletions
84
views_api.py
84
views_api.py
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue