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

View File

@ -1,3 +1,4 @@
import asyncio
import logging
from typing import List
@ -6,7 +7,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from backend import actions
from backend import actions, enforcer, rules
from backend.actions import ActionError
from backend.auth import (
authenticate_admin_user,
@ -17,12 +18,14 @@ from backend.auth import (
list_manageable_users,
)
from backend.models import (
AccessRule,
ActionRequest,
ActionResponse,
EnrollRequest,
EnrollResponse,
LoginRequest,
LoginResponse,
RuleSet,
UpdateActionResponse,
UpdateCheckResponse,
UpdateLogEntry,
@ -48,6 +51,11 @@ app.mount("/assets", StaticFiles(directory="assets"), name="assets")
templates = Jinja2Templates(directory="backend/templates")
@app.on_event("startup")
async def startup_event():
asyncio.create_task(enforcer.enforcement_loop())
def get_oidc_client(settings: Settings = Depends(get_settings)) -> OIDCClient:
if not settings.oidc_enabled:
raise HTTPException(
@ -238,6 +246,32 @@ def enable_user(
)
@app.get("/rules", response_model=RuleSet, dependencies=[Depends(get_current_admin)])
def get_rules(settings: Settings = Depends(get_settings)) -> RuleSet:
manager = rules.RuleManager(settings)
return manager.load_rules()
@app.post("/rules", dependencies=[Depends(get_current_admin)])
def set_rule(
rule: AccessRule,
settings: Settings = Depends(get_settings),
) -> dict:
manager = rules.RuleManager(settings)
manager.set_rule(rule)
return {"status": "ok"}
@app.delete("/rules/{username}", dependencies=[Depends(get_current_admin)])
def delete_rule(
username: str,
settings: Settings = Depends(get_settings),
) -> dict:
manager = rules.RuleManager(settings)
manager.delete_rule(username)
return {"status": "ok"}
@app.get("/update/status", response_model=UpdateStatus, dependencies=[Depends(get_current_admin)])
def update_status(settings: Settings = Depends(get_settings)) -> UpdateStatus:
status_data = update.get_status(settings)
@ -334,3 +368,9 @@ def system_metrics_status() -> SystemMetrics:
@app.get("/dashboard", response_class=HTMLResponse)
def index(request: Request) -> HTMLResponse:
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/ui/rules", response_class=HTMLResponse)
def rules_ui(request: Request) -> HTMLResponse:
return templates.TemplateResponse("rules.html", {"request": request})