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:
91
backend/rules.py
Normal file
91
backend/rules.py
Normal file
@ -0,0 +1,91 @@
|
||||
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
|
||||
Reference in New Issue
Block a user