Files
kiddo/backend/rules.py
stephan 63ee0b40b5 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
2026-01-16 11:25:56 +01:00

92 lines
3.1 KiB
Python

import json
import logging
import os
from datetime import datetime
from typing import Optional
from backend.models import RuleSet, AccessRule, DayOfWeek
from backend.settings import get_settings, Settings
logger = logging.getLogger(__name__)
class RuleManager:
def __init__(self, settings: Optional[Settings] = None):
self.settings = settings or get_settings()
self.rules_file = self.settings.rules_file
def load_rules(self) -> RuleSet:
if not os.path.exists(self.rules_file):
return RuleSet()
try:
with open(self.rules_file, "r", encoding="utf-8") as f:
data = json.load(f)
return RuleSet(**data)
except (json.JSONDecodeError, OSError) as e:
logger.error("Failed to load rules from %s: %s", self.rules_file, e)
return RuleSet()
def save_rules(self, rules: RuleSet) -> None:
try:
os.makedirs(os.path.dirname(self.rules_file), exist_ok=True)
with open(self.rules_file, "w", encoding="utf-8") as f:
f.write(rules.model_dump_json(indent=2))
except OSError as e:
logger.error("Failed to save rules to %s: %s", self.rules_file, e)
raise
def get_rule(self, user: str) -> Optional[AccessRule]:
rules = self.load_rules()
return rules.rules.get(user)
def set_rule(self, rule: AccessRule) -> None:
rules = self.load_rules()
rules.rules[rule.user] = rule
self.save_rules(rules)
def delete_rule(self, user: str) -> None:
rules = self.load_rules()
if user in rules.rules:
del rules.rules[user]
self.save_rules(rules)
def is_login_allowed(self, user: str, now: Optional[datetime] = None) -> bool:
"""
Check if user is allowed to login at the given time (or now).
Returns True if no rule exists (or disabled) or if time is within an allowed window.
Returns False if a rule exists and current time is outside all allowed windows.
"""
rule = self.get_rule(user)
if not rule or not rule.enabled:
# No rule or rule disabled -> Default Allow
return True
if not rule.allowed_windows:
# Rule enabled but no windows -> Deny All
return False
if now is None:
now = datetime.now()
# Map weekday to Enum
weekday_map = {
0: DayOfWeek.MONDAY,
1: DayOfWeek.TUESDAY,
2: DayOfWeek.WEDNESDAY,
3: DayOfWeek.THURSDAY,
4: DayOfWeek.FRIDAY,
5: DayOfWeek.SATURDAY,
6: DayOfWeek.SUNDAY,
}
current_day = weekday_map[now.weekday()]
current_time_str = now.strftime("%H:%M")
for window in rule.allowed_windows:
if current_day in window.days:
# Handle simple range start <= now <= end
# (Overnight windows assumed to be split by user into two windows)
if window.start_time <= current_time_str <= window.end_time:
return True
return False