cteward-ng/cteward_ng/tests/test_permissions.py
2026-06-06 22:21:20 +02:00

229 lines
7.8 KiB
Python

"""Tests for flag-based permission resolution (permissions.py).
Replaces test/authprovider-find_config_flags.js,
test/authprovider-find_database_flags.js,
test/authprovider-impersonate.js,
test/authprovider-effective_permissions.js.
"""
from unittest.mock import Mock, patch
import pytest
from cteward_ng.permissions import (
find_config_flags,
find_database_flags,
impersonate,
effective_permissions,
)
# ── helpers ───────────────────────────────────────────────────────
def _make_request(query_params=None, view_args=None):
req = Mock()
req.args = Mock()
req.args.get = Mock(return_value=query_params)
req.view_args = view_args
return req
# ── find_config_flags ─────────────────────────────────────────────
class TestFindConfigFlags:
def test_basic_flags(self):
ctx = {
'username': 'alice',
'request': _make_request(view_args={'crewname': 'bob'}),
'config': {'auth': {'flags': {'alice': ['_board_', '_admin_']}}},
'flags': ['_anonymous_'],
}
find_config_flags(ctx)
assert '_anonymous_' in ctx['flags']
assert '_board_' in ctx['flags']
assert '_admin_' in ctx['flags']
def test_self_flag_when_querying_own_record(self):
ctx = {
'username': 'alice',
'request': _make_request(view_args={'crewname': 'alice'}),
'config': {'auth': {'flags': {}}},
'flags': ['_anonymous_'],
}
find_config_flags(ctx)
assert '_self_' in ctx['flags']
def test_no_self_flag_when_querying_other(self):
ctx = {
'username': 'alice',
'request': _make_request(view_args={'crewname': 'bob'}),
'config': {'auth': {'flags': {}}},
'flags': ['_anonymous_'],
}
find_config_flags(ctx)
assert '_self_' not in ctx['flags']
def test_impersonating_limited_strips_privileges(self):
ctx = {
'username': 'limiter',
'request': _make_request(view_args={'crewname': 'someone'}),
'config': {'auth': {'flags': {'limiter': ['_admin_', '_board_', '_bot_', '_impersonate_']}}},
'flags': ['_anonymous_', '_impersonate_'],
}
find_config_flags(ctx)
# After stripping, none of admin/board/bot should remain
for bad in ('_admin_', '_board_', '_bot_'):
assert bad not in ctx['flags'], f"{bad} should have been stripped"
def test_no_view_args(self):
ctx = {
'username': 'alice',
'request': _make_request(view_args=None),
'config': {'auth': {'flags': {'alice': ['_bot_']}}},
'flags': ['_anonymous_'],
}
find_config_flags(ctx)
assert '_bot_' in ctx['flags']
# ── find_database_flags ───────────────────────────────────────
class TestFindDatabaseFlags:
@patch('cteward_ng.permissions.member_lookup')
def test_crew_gets_member_flag(self, mock_lookup):
mock_lookup.return_value = {'Kennung3': 'crew', 'Kurzname': 'alice'}
ctx = {
'username': 'alice',
'flags': ['_anonymous_'],
}
find_database_flags(ctx)
assert '_member_' in ctx['flags']
@patch('cteward_ng.permissions.member_lookup')
def test_raumfahrer_gets_astronaut_flag(self, mock_lookup):
mock_lookup.return_value = {'Kennung3': 'raumfahrer', 'Kurzname': 'astronaut'}
ctx = {
'username': 'astronaut',
'flags': ['_anonymous_'],
}
find_database_flags(ctx)
assert '_astronaut_' in ctx['flags']
@patch('cteward_ng.permissions.member_lookup')
def test_passiv_gets_passive_flag(self, mock_lookup):
mock_lookup.return_value = {'Kennung3': 'passiv', 'Kurzname': 'oldtimer'}
ctx = {
'username': 'oldtimer',
'flags': ['_anonymous_'],
}
find_database_flags(ctx)
assert '_passive_' in ctx['flags']
@patch('cteward_ng.permissions.member_lookup')
def test_not_in_db_no_extra_flags(self, mock_lookup):
mock_lookup.side_effect = RuntimeError("Not found.")
ctx = {
'username': 'nobody',
'flags': ['_anonymous_'],
}
find_database_flags(ctx)
assert ctx['flags'] == ['_anonymous_']
# ── impersonate ───────────────────────────────────────────────
class TestImpersonate:
def test_no_impersonate_param(self):
ctx = {
'username': 'alice',
'flags': ['_anonymous_'],
'request': _make_request(query_params=None),
}
impersonate(ctx)
assert ctx['username'] == 'alice'
def test_impersonate_self_is_noop(self):
ctx = {
'username': 'alice',
'flags': ['_anonymous_'],
'request': _make_request(query_params='alice'),
}
impersonate(ctx)
assert ctx['username'] == 'alice'
def test_unauthorized_impersonation_aborts(self):
ctx = {
'username': 'nobody',
'flags': ['_anonymous_'],
'request': _make_request(query_params='admin'),
}
with pytest.raises(Exception):
impersonate(ctx)
@patch('cteward_ng.permissions.find_config_flags')
@patch('cteward_ng.permissions.find_database_flags')
def test_admin_can_impersonate(self, mock_dbf, mock_cfg):
ctx = {
'username': 'admin',
'flags': ['_anonymous_', '_admin_'],
'request': _make_request(query_params='target_user'),
'config': {'auth': {'flags': {}}},
}
impersonate(ctx)
assert ctx['username'] == 'target_user'
mock_cfg.assert_called_once()
mock_dbf.assert_called_once()
# ── effective_permissions ─────────────────────────────────────
class TestEffectivePermissions:
def test_lowest_level_wins(self):
ctx = {
'username': 'alice',
'flags': ['_anonymous_', '_board_'],
'permissions': {
'_anonymous_': {'query': 'Q1', 'level': 3},
'_board_': {'query': 'Q2', 'level': 0},
},
}
effective_permissions(ctx)
assert ctx['permission']['query'] == 'Q2'
assert ctx['query'] == 'Q2'
def test_anonymous_only(self):
ctx = {
'username': 'anon',
'flags': ['_anonymous_'],
'permissions': {
'_anonymous_': {'query': 'Q1', 'level': 3},
},
}
effective_permissions(ctx)
assert ctx['permission']['query'] == 'Q1'
def test_no_matching_permission_aborts(self):
ctx = {
'username': 'nobody',
'flags': ['_anonymous_'],
'permissions': {
'_board_': {'query': 'Q1', 'level': 0},
},
}
with pytest.raises(Exception):
effective_permissions(ctx)
def test_permission_with_filter(self):
from cteward_ng.filters import memberlist_active_only
ctx = {
'username': 'alice',
'flags': ['_anonymous_', '_member_'],
'permissions': {
'_member_': {
'query': 'Q1',
'level': 1,
'filter': memberlist_active_only,
},
},
}
effective_permissions(ctx)
assert ctx['filter'] is memberlist_active_only