feat: login time window rules (EPIC_000014)

- Implemented RuleManager and JSON storage
- Added background enforcement scheduler
- Added Web UI for rule management at /ui/rules
- Bumped version to 0.3.0
This commit is contained in:
2026-01-16 11:25:56 +01:00
parent 9ad9501722
commit 63ee0b40b5
16 changed files with 851 additions and 16 deletions

56
tests/test_api_rules.py Normal file
View File

@ -0,0 +1,56 @@
import unittest
import os
import sys
import tempfile
from unittest.mock import MagicMock
# Mock pam module before importing backend components
sys.modules["pam"] = MagicMock()
from backend.settings import Settings
from backend.models import AccessRule, TimeWindow, DayOfWeek
# Import endpoint functions directly
from backend.app import get_rules, set_rule, delete_rule
class TestRulesDirect(unittest.TestCase):
def setUp(self):
self.tmp_fd, self.rules_file = tempfile.mkstemp()
os.close(self.tmp_fd)
self.settings = Settings()
self.settings.rules_file = self.rules_file
def tearDown(self):
if os.path.exists(self.rules_file):
os.remove(self.rules_file)
def test_crud_rules_direct(self):
# 1. Get empty rules
rules = get_rules(settings=self.settings)
self.assertEqual(rules.rules, {})
# 2. Create rule
rule = AccessRule(
user="kid",
enabled=True,
allowed_windows=[
TimeWindow(days=[DayOfWeek.MONDAY], start_time="14:00", end_time="16:00")
]
)
set_rule(rule, settings=self.settings)
# 3. Verify created
rules = get_rules(settings=self.settings)
self.assertIn("kid", rules.rules)
self.assertEqual(rules.rules["kid"].allowed_windows[0].start_time, "14:00")
# 4. Delete rule
delete_rule("kid", settings=self.settings)
# 5. Verify deleted
rules = get_rules(settings=self.settings)
self.assertNotIn("kid", rules.rules)
if __name__ == "__main__":
unittest.main()

95
tests/test_rules.py Normal file
View File

@ -0,0 +1,95 @@
import unittest
import os
import tempfile
from datetime import datetime
from unittest.mock import patch
# Adjust path if necessary, assuming run from project root
from backend.models import AccessRule, TimeWindow, DayOfWeek
from backend.rules import RuleManager
class TestRules(unittest.TestCase):
def setUp(self):
self.tmp_fd, self.rules_file = tempfile.mkstemp()
os.close(self.tmp_fd)
def tearDown(self):
if os.path.exists(self.rules_file):
os.remove(self.rules_file)
@patch("backend.rules.get_settings")
def test_load_save_rules(self, mock_settings):
mock_settings.return_value.rules_file = self.rules_file
manager = RuleManager()
# Should be empty initially
rules = manager.load_rules()
self.assertEqual(rules.rules, {})
# Save a rule
rule = AccessRule(
user="testuser",
allowed_windows=[
TimeWindow(days=[DayOfWeek.MONDAY], start_time="09:00", end_time="17:00")
]
)
manager.set_rule(rule)
# Load back
loaded = manager.get_rule("testuser")
self.assertIsNotNone(loaded)
self.assertEqual(loaded.user, "testuser")
self.assertEqual(loaded.allowed_windows[0].start_time, "09:00")
@patch("backend.rules.get_settings")
def test_access_logic(self, mock_settings):
mock_settings.return_value.rules_file = self.rules_file
manager = RuleManager()
# No rule -> Allowed
self.assertTrue(manager.is_login_allowed("unknown"))
# Add rule: Mon 10-12
rule = AccessRule(
user="kid",
allowed_windows=[
TimeWindow(days=[DayOfWeek.MONDAY], start_time="10:00", end_time="12:00")
]
)
manager.set_rule(rule)
# Monday 11:00 -> Allowed
# Jan 2 2023 is Monday
mon_11 = datetime(2023, 1, 2, 11, 0)
self.assertTrue(manager.is_login_allowed("kid", mon_11))
# Monday 09:00 -> Denied
mon_09 = datetime(2023, 1, 2, 9, 0)
self.assertFalse(manager.is_login_allowed("kid", mon_09))
# Tuesday 11:00 -> Denied
# Jan 3 2023 is Tuesday
tue_11 = datetime(2023, 1, 3, 11, 0)
self.assertFalse(manager.is_login_allowed("kid", tue_11))
@patch("backend.rules.get_settings")
def test_disabled_rule(self, mock_settings):
mock_settings.return_value.rules_file = self.rules_file
manager = RuleManager()
# Add disabled rule
rule = AccessRule(
user="kid",
enabled=False,
allowed_windows=[
TimeWindow(days=[DayOfWeek.MONDAY], start_time="10:00", end_time="12:00")
]
)
manager.set_rule(rule)
# Should be allowed even if time doesn't match, because rule is disabled (default allow)
mon_09 = datetime(2023, 1, 2, 9, 0)
self.assertTrue(manager.is_login_allowed("kid", mon_09))
if __name__ == "__main__":
unittest.main()