feat: enhance project and scenario management with role-based access control

- Implemented role-based access control for project and scenario routes.
- Added authorization checks to ensure users have appropriate roles for viewing and managing projects and scenarios.
- Introduced utility functions for ensuring project and scenario access based on user roles.
- Refactored project and scenario routes to utilize new authorization helpers.
- Created initial data seeding script to set up default roles and an admin user.
- Added tests for authorization helpers and initial data seeding functionality.
- Updated exception handling to include authorization errors.
This commit is contained in:
2025-11-09 23:14:54 +01:00
parent 27262bdfa3
commit 0f79864188
16 changed files with 997 additions and 132 deletions

View File

@@ -0,0 +1,156 @@
from __future__ import annotations
import logging
from collections.abc import Callable
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from config.database import Base
from scripts import initial_data
from scripts.initial_data import AdminSeedResult, RoleSeedResult, SeedConfig
from services.repositories import DEFAULT_ROLE_DEFINITIONS
from services.unit_of_work import UnitOfWork
@pytest.fixture()
def in_memory_session_factory() -> Callable[[], Session]:
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
def _session_factory() -> Session:
return factory()
return _session_factory
@pytest.fixture()
def uow(in_memory_session_factory: Callable[[], Session]) -> UnitOfWork:
return UnitOfWork(session_factory=in_memory_session_factory)
def test_ensure_default_roles_idempotent(uow: UnitOfWork) -> None:
with uow as working:
assert working.roles is not None
result_first = initial_data.ensure_default_roles(working.roles, DEFAULT_ROLE_DEFINITIONS)
assert result_first == RoleSeedResult(created=4, updated=0, total=4)
with uow as working:
assert working.roles is not None
result_second = initial_data.ensure_default_roles(working.roles, DEFAULT_ROLE_DEFINITIONS)
assert result_second == RoleSeedResult(created=0, updated=0, total=4)
def test_ensure_admin_user_creates_and_assigns_roles(uow: UnitOfWork) -> None:
config = SeedConfig(
admin_email="admin@example.com",
admin_username="admin",
admin_password="secret",
admin_roles=("admin", "viewer"),
force_reset=False,
)
with uow as working:
assert working.roles is not None
assert working.users is not None
initial_data.ensure_default_roles(working.roles, DEFAULT_ROLE_DEFINITIONS)
result = initial_data.ensure_admin_user(working.users, working.roles, config)
assert result == AdminSeedResult(
created_user=True,
updated_user=False,
password_rotated=False,
roles_granted=2,
)
with uow as working:
assert working.roles is not None
assert working.users is not None
result_again = initial_data.ensure_admin_user(working.users, working.roles, config)
assert result_again == AdminSeedResult(
created_user=False,
updated_user=False,
password_rotated=False,
roles_granted=0,
)
with uow as working:
assert working.users is not None
user = working.users.get_by_email("admin@example.com", with_roles=True)
assert user is not None
assert user.is_active is True
assert user.is_superuser is True
role_names = {role.name for role in user.roles}
assert role_names == {"admin", "viewer"}
def test_ensure_admin_user_force_reset_rotates_password(uow: UnitOfWork) -> None:
base_config = SeedConfig(
admin_email="admin@example.com",
admin_username="admin",
admin_password="first",
admin_roles=("admin",),
force_reset=False,
)
with uow as working:
assert working.roles is not None
assert working.users is not None
initial_data.ensure_default_roles(working.roles, DEFAULT_ROLE_DEFINITIONS)
initial_data.ensure_admin_user(working.users, working.roles, base_config)
rotate_config = SeedConfig(
admin_email="admin@example.com",
admin_username="admin",
admin_password="second",
admin_roles=("admin",),
force_reset=True,
)
with uow as working:
assert working.users is not None
user_before = working.users.get_by_email("admin@example.com")
assert user_before is not None
old_hash = user_before.password_hash
with uow as working:
assert working.roles is not None
assert working.users is not None
result = initial_data.ensure_admin_user(working.users, working.roles, rotate_config)
assert result.password_rotated is True
with uow as working:
assert working.users is not None
user_after = working.users.get_by_email("admin@example.com")
assert user_after is not None
assert user_after.password_hash != old_hash
def test_seed_initial_data_logs_results(
caplog,
in_memory_session_factory: Callable[[], Session],
) -> None:
caplog.set_level(logging.INFO)
config = SeedConfig(
admin_email="seed@example.com",
admin_username="seed",
admin_password="seed-pass",
admin_roles=("admin",),
force_reset=False,
)
initial_data.seed_initial_data(
config,
unit_of_work_factory=lambda: UnitOfWork(session_factory=in_memory_session_factory),
)
assert "Starting initial data seeding" in caplog.text
assert "Initial data seeding completed successfully" in caplog.text
with UnitOfWork(session_factory=in_memory_session_factory) as check_uow:
assert check_uow.users is not None
assert check_uow.roles is not None
user = check_uow.users.get_by_email("seed@example.com")
assert user is not None
assert check_uow.roles.get_by_name("admin") is not None