Files
pvemanager/backend/tests/test_migration_setting_view_split.py
Dilshod Rasulov bddbf15652
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m22s
Deploy to Production / Trigger production update (push) Failing after 11s
feat(rbac): split setting:view into panel settings and a permission-free profile
setting:view used to gate the whole /settings page, which mixed panel-wide
configuration with a user's own account. Revoking it to keep a standard user
out of the panel settings also took away their profile, password, 2FA and SSH
keys, so the permission could not be used for what it names.

The account half now lives at /profile: its endpoints are self-scoped and only
require a session, so the page carries no permission at all and is reached from
the user menu (and Ctrl+K) rather than a sidebar group. /settings keeps the
panel-wide tabs behind setting:view, with saving gated on setting:update,
security on setting:manage and the SMTP/Telegram channels on is_admin, matching
what each endpoint checks.

The shipped "user" role no longer gets setting:view. Migration 42 drops it from
roles holding it without setting:update/manage, since those grants only existed
to reach the profile page; it runs once (marker in panel_settings) so a later
deliberate grant is not reverted on the next start.
2026-08-11 20:07:34 +05:00

86 lines
2.5 KiB
Python

"""Tests for the setting:view split (migration 42).
``table_exists`` queries ``information_schema`` and is therefore
PostgreSQL-only; it is stubbed so the migration itself — whose statements are
portable — can run against the SQLite test database.
"""
from __future__ import annotations
import json
import pytest
from sqlalchemy import text
from migrations import migrations as mig
from migrations.migrations import migrate_setting_view_panel_only
from app.models import Role
@pytest.fixture(autouse=True)
def postgres_only_checks(monkeypatch):
monkeypatch.setattr(mig, "table_exists", lambda conn, table: True)
def _run(db_session):
"""Run the migration on a connection sharing the session's transaction."""
migrate_setting_view_panel_only(db_session.connection())
def _role(db_session, name, permissions):
role = Role(
name=name, display_name=name.title(), permissions=permissions,
is_system=False, is_active=True,
)
db_session.add(role)
db_session.commit()
return role
def _perms(db_session, name) -> dict:
raw = db_session.connection().execute(
text("SELECT permissions FROM roles WHERE name = :n"), {"n": name}
).scalar()
return raw if isinstance(raw, dict) else json.loads(raw or "{}")
def test_view_only_role_loses_the_grant(db_session):
_role(db_session, "viewer", {"vm:view": True, "setting:view": True})
_run(db_session)
assert "setting:view" not in _perms(db_session, "viewer")
assert _perms(db_session, "viewer")["vm:view"] is True
def test_panel_administrator_keeps_the_grant(db_session):
_role(db_session, "ops", {"setting:view": True, "setting:update": True})
_role(db_session, "sec", {"setting:view": True, "setting:manage": True})
_run(db_session)
assert _perms(db_session, "ops")["setting:view"] is True
assert _perms(db_session, "sec")["setting:view"] is True
def test_role_without_the_grant_is_untouched(db_session):
_role(db_session, "plain", {"vm:view": True})
_run(db_session)
assert _perms(db_session, "plain") == {"vm:view": True}
def test_runs_once_so_a_later_grant_survives(db_session):
_role(db_session, "viewer", {"setting:view": True})
_run(db_session)
# An admin re-grants it deliberately after the upgrade.
db_session.connection().execute(
text("UPDATE roles SET permissions = :p WHERE name = 'viewer'"),
{"p": json.dumps({"setting:view": True})},
)
_run(db_session)
assert _perms(db_session, "viewer")["setting:view"] is True