316 lines
10 KiB
Python
316 lines
10 KiB
Python
"""
|
|
Unit tests for rbac.py - Role-based access control.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
|
|
from rbac import (
|
|
DEFAULT_RBAC,
|
|
ROLE_GROUP_MAP,
|
|
get_pages,
|
|
get_sidebar_links,
|
|
get_sidebar_order,
|
|
load_rbac,
|
|
resolve_role,
|
|
save_rbac,
|
|
update_rbac,
|
|
)
|
|
|
|
|
|
class TestLoadRBAC:
|
|
"""Test loading RBAC configuration."""
|
|
|
|
def test_load_rbac_from_file(self, mock_config, temp_rbac_file):
|
|
"""Test loading RBAC from existing file."""
|
|
rbac = load_rbac()
|
|
|
|
assert "role_defaults" in rbac
|
|
assert "user_overrides" in rbac
|
|
assert "admin" in rbac["role_defaults"]
|
|
assert "member" in rbac["role_defaults"]
|
|
assert "viewer" in rbac["role_defaults"]
|
|
|
|
def test_load_rbac_missing_file(self, mock_config, temp_volume_root):
|
|
"""Test loading RBAC when file doesn't exist returns defaults."""
|
|
# Don't create rbac file
|
|
rbac = load_rbac()
|
|
|
|
assert rbac == DEFAULT_RBAC
|
|
|
|
def test_load_rbac_corrupted_file(self, mock_config, temp_volume_root):
|
|
"""Test loading RBAC with corrupted file returns defaults."""
|
|
rbac_file = os.path.join(temp_volume_root, "docker/nas-dashboard/rbac.json")
|
|
os.makedirs(os.path.dirname(rbac_file), exist_ok=True)
|
|
|
|
# Write invalid JSON
|
|
with open(rbac_file, "w") as f:
|
|
f.write("invalid json {{{")
|
|
|
|
rbac = load_rbac()
|
|
assert rbac == DEFAULT_RBAC
|
|
|
|
|
|
class TestSaveRBAC:
|
|
"""Test saving RBAC configuration."""
|
|
|
|
def test_save_rbac(self, mock_config, temp_volume_root):
|
|
"""Test saving RBAC configuration."""
|
|
rbac_file = os.path.join(temp_volume_root, "docker/nas-dashboard/rbac.json")
|
|
test_data = {
|
|
"role_defaults": {"admin": {"pages": "*"}},
|
|
"user_overrides": {"testuser": {"pages": ["dashboard"]}},
|
|
}
|
|
|
|
save_rbac(test_data)
|
|
|
|
assert os.path.exists(rbac_file)
|
|
with open(rbac_file) as f:
|
|
loaded = json.load(f)
|
|
assert loaded == test_data
|
|
|
|
def test_save_rbac_creates_directory(self, mock_config, temp_volume_root):
|
|
"""Test that save_rbac creates directory if it doesn't exist."""
|
|
rbac_file = os.path.join(temp_volume_root, "docker/nas-dashboard/rbac.json")
|
|
|
|
# Remove directory
|
|
import shutil
|
|
|
|
if os.path.exists(os.path.dirname(rbac_file)):
|
|
shutil.rmtree(os.path.dirname(rbac_file))
|
|
|
|
test_data = {"role_defaults": {}, "user_overrides": {}}
|
|
save_rbac(test_data)
|
|
|
|
assert os.path.exists(rbac_file)
|
|
|
|
|
|
class TestUpdateRBAC:
|
|
"""Test atomic RBAC updates."""
|
|
|
|
def test_update_rbac_with_mutator(self, mock_config, temp_rbac_file):
|
|
"""Test updating RBAC with mutator function."""
|
|
|
|
def add_user_override(data):
|
|
data["user_overrides"]["newuser"] = {"pages": ["dashboard", "docker"]}
|
|
return "success"
|
|
|
|
result = update_rbac(add_user_override)
|
|
|
|
assert result == "success"
|
|
|
|
# Verify changes persisted
|
|
rbac = load_rbac()
|
|
assert "newuser" in rbac["user_overrides"]
|
|
assert rbac["user_overrides"]["newuser"]["pages"] == ["dashboard", "docker"]
|
|
|
|
def test_update_rbac_thread_safe(self, mock_config, temp_rbac_file):
|
|
"""Test that update_rbac is thread-safe."""
|
|
import threading
|
|
|
|
results = []
|
|
|
|
def increment_counter(data):
|
|
current = data.get("counter", 0)
|
|
data["counter"] = current + 1
|
|
return data["counter"]
|
|
|
|
# Run multiple updates concurrently
|
|
threads = []
|
|
for _ in range(10):
|
|
t = threading.Thread(target=lambda: results.append(update_rbac(increment_counter)))
|
|
threads.append(t)
|
|
t.start()
|
|
|
|
for t in threads:
|
|
t.join()
|
|
|
|
# Final counter should be 10
|
|
rbac = load_rbac()
|
|
assert rbac.get("counter") == 10
|
|
|
|
|
|
class TestResolveRole:
|
|
"""Test role resolution from groups."""
|
|
|
|
def test_resolve_role_admin(self):
|
|
"""Test resolving admin role."""
|
|
groups = ["dashboard_admin", "other_group"]
|
|
role = resolve_role(groups)
|
|
assert role == "admin"
|
|
|
|
def test_resolve_role_member(self):
|
|
"""Test resolving member role."""
|
|
groups = ["dashboard_member"]
|
|
role = resolve_role(groups)
|
|
assert role == "member"
|
|
|
|
def test_resolve_role_viewer(self):
|
|
"""Test resolving viewer role."""
|
|
groups = ["dashboard_viewer", "unrelated_group"]
|
|
role = resolve_role(groups)
|
|
assert role == "viewer"
|
|
|
|
def test_resolve_role_no_match(self):
|
|
"""Test resolving role with no matching groups."""
|
|
groups = ["other_group", "another_group"]
|
|
role = resolve_role(groups)
|
|
assert role is None
|
|
|
|
def test_resolve_role_empty_groups(self):
|
|
"""Test resolving role with empty groups list."""
|
|
role = resolve_role([])
|
|
assert role is None
|
|
|
|
def test_resolve_role_priority(self):
|
|
"""Test that first matching group is used."""
|
|
groups = ["dashboard_admin", "dashboard_member"]
|
|
role = resolve_role(groups)
|
|
assert role == "admin"
|
|
|
|
|
|
class TestGetPages:
|
|
"""Test page access resolution."""
|
|
|
|
def test_get_pages_admin_default(self, mock_config, temp_rbac_file):
|
|
"""Test getting pages for admin with default settings."""
|
|
pages = get_pages("testuser", "admin")
|
|
assert pages == "*"
|
|
|
|
def test_get_pages_member_default(self, mock_config, temp_rbac_file):
|
|
"""Test getting pages for member with default settings."""
|
|
pages = get_pages("testuser", "member")
|
|
assert isinstance(pages, list)
|
|
assert "dashboard" in pages
|
|
|
|
def test_get_pages_viewer_default(self, mock_config, temp_rbac_file):
|
|
"""Test getting pages for viewer with default settings."""
|
|
pages = get_pages("testuser", "viewer")
|
|
assert pages == ["dashboard"]
|
|
|
|
def test_get_pages_with_user_override(self, mock_config, temp_rbac_file):
|
|
"""Test getting pages with user-specific override."""
|
|
|
|
# Add user override
|
|
def add_override(data):
|
|
data["user_overrides"]["customuser"] = {"pages": ["dashboard", "files"]}
|
|
|
|
update_rbac(add_override)
|
|
|
|
pages = get_pages("customuser", "admin")
|
|
assert pages == ["dashboard", "files"]
|
|
|
|
def test_get_pages_unknown_role(self, mock_config, temp_rbac_file):
|
|
"""Test getting pages for unknown role returns empty list."""
|
|
pages = get_pages("testuser", "unknown_role")
|
|
assert pages == []
|
|
|
|
|
|
class TestGetSidebarLinks:
|
|
"""Test sidebar link access resolution."""
|
|
|
|
def test_get_sidebar_links_admin_default(self, mock_config, temp_rbac_file):
|
|
"""Test getting sidebar links for admin."""
|
|
links = get_sidebar_links("testuser", "admin")
|
|
assert links == "*"
|
|
|
|
def test_get_sidebar_links_member_default(self, mock_config, temp_rbac_file):
|
|
"""Test getting sidebar links for member."""
|
|
links = get_sidebar_links("testuser", "member")
|
|
assert links == "*"
|
|
|
|
def test_get_sidebar_links_viewer_default(self, mock_config, temp_rbac_file):
|
|
"""Test getting sidebar links for viewer."""
|
|
links = get_sidebar_links("testuser", "viewer")
|
|
assert links == ["dashboard"]
|
|
|
|
def test_get_sidebar_links_with_user_override(self, mock_config, temp_rbac_file):
|
|
"""Test getting sidebar links with user override."""
|
|
|
|
def add_override(data):
|
|
data["user_overrides"]["customuser"] = {"pages": "*", "sidebar_links": ["dashboard", "docker", "files"]}
|
|
|
|
update_rbac(add_override)
|
|
|
|
links = get_sidebar_links("customuser", "admin")
|
|
assert links == ["dashboard", "docker", "files"]
|
|
|
|
def test_get_sidebar_links_no_override(self, mock_config, temp_rbac_file):
|
|
"""Test that user without sidebar_links override gets role default."""
|
|
|
|
def add_override(data):
|
|
data["user_overrides"]["customuser"] = {"pages": ["dashboard"]}
|
|
# No sidebar_links specified
|
|
|
|
update_rbac(add_override)
|
|
|
|
links = get_sidebar_links("customuser", "admin")
|
|
assert links == "*" # Should fall back to admin default
|
|
|
|
|
|
class TestGetSidebarOrder:
|
|
"""Test sidebar order retrieval."""
|
|
|
|
def test_get_sidebar_order_no_override(self, mock_config, temp_rbac_file):
|
|
"""Test getting sidebar order when user has no override."""
|
|
order = get_sidebar_order("testuser")
|
|
assert order is None
|
|
|
|
def test_get_sidebar_order_with_override(self, mock_config, temp_rbac_file):
|
|
"""Test getting sidebar order with user override."""
|
|
custom_order = {"Main": ["dashboard", "docker"], "Media": ["navidrome", "immich"]}
|
|
|
|
def add_override(data):
|
|
data["user_overrides"]["customuser"] = {"sidebar_order": custom_order}
|
|
|
|
update_rbac(add_override)
|
|
|
|
order = get_sidebar_order("customuser")
|
|
assert order == custom_order
|
|
|
|
|
|
class TestDefaultRBAC:
|
|
"""Test default RBAC configuration."""
|
|
|
|
def test_default_rbac_structure(self):
|
|
"""Test that DEFAULT_RBAC has correct structure."""
|
|
assert "role_defaults" in DEFAULT_RBAC
|
|
assert "user_overrides" in DEFAULT_RBAC
|
|
|
|
assert "admin" in DEFAULT_RBAC["role_defaults"]
|
|
assert "member" in DEFAULT_RBAC["role_defaults"]
|
|
assert "viewer" in DEFAULT_RBAC["role_defaults"]
|
|
|
|
def test_default_rbac_admin_permissions(self):
|
|
"""Test admin has full permissions by default."""
|
|
admin = DEFAULT_RBAC["role_defaults"]["admin"]
|
|
assert admin["pages"] == "*"
|
|
assert admin["sidebar_links"] == "*"
|
|
|
|
def test_default_rbac_member_permissions(self):
|
|
"""Test member has limited permissions."""
|
|
member = DEFAULT_RBAC["role_defaults"]["member"]
|
|
assert isinstance(member["pages"], list)
|
|
assert "dashboard" in member["pages"]
|
|
assert member["sidebar_links"] == "*"
|
|
|
|
def test_default_rbac_viewer_permissions(self):
|
|
"""Test viewer has minimal permissions."""
|
|
viewer = DEFAULT_RBAC["role_defaults"]["viewer"]
|
|
assert viewer["pages"] == ["dashboard"]
|
|
assert viewer["sidebar_links"] == ["dashboard"]
|
|
|
|
|
|
class TestRoleGroupMap:
|
|
"""Test role group mapping."""
|
|
|
|
def test_role_group_map_completeness(self):
|
|
"""Test that ROLE_GROUP_MAP has all expected mappings."""
|
|
assert "dashboard_admin" in ROLE_GROUP_MAP
|
|
assert "dashboard_member" in ROLE_GROUP_MAP
|
|
assert "dashboard_viewer" in ROLE_GROUP_MAP
|
|
|
|
assert ROLE_GROUP_MAP["dashboard_admin"] == "admin"
|
|
assert ROLE_GROUP_MAP["dashboard_member"] == "member"
|
|
assert ROLE_GROUP_MAP["dashboard_viewer"] == "viewer"
|