diff --git a/CHANGELOG.md b/CHANGELOG.md index e6725c6..5657566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ By: Codex (GPT-5) | 28.12.2025 | πŸ—οΈ Planning | ID: PR-Vorbereitung fuer feature/oidc-validation geplant. By: Codex (GPT-5) | | 28.12.2025 | πŸ—οΈ Planning | ID: PAM immer aktiv; OIDC optional mit deaktivierter UI-Option dokumentiert. By: Codex (GPT-5) | | 28.12.2025 | βš™οΈ Code | ID: Installer setzt PAM-Defaults in /etc/skd/env. By: Codex (GPT-5) | +| 28.12.2025 | βš™οΈ Code | ID: Root-Service auf Port 80, Account-Status und Self-Disable-Schutz. By: Codex (GPT-5) | --- ## Legende diff --git a/Makefile b/Makefile index 05b4a2c..34d031a 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ ENV_FILE ?= $(ENV_DIR)/env SYSTEMD_PATH ?= /etc/systemd/system/$(SERVICE).service BRANCH ?= main HOST ?= 127.0.0.1 -PORT ?= 8000 +PORT ?= 80 HEALTH_URL ?= http://$(HOST):$(PORT)/health TOKEN ?= $(shell awk -F= '/^SKD_AUTH_TOKEN=/{print $$2}' $(ENV_FILE) 2>/dev/null) KEEP_INSTALL_DIR ?= 1 diff --git a/README.md b/README.md index ad8659c..1563bac 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ cd /opt/sk ./scripts/install.sh sudo systemctl status skd.service ``` -Then open `http://localhost:8000/` and log in via PAM (default) to start quickly. +Then open `http://localhost/` and log in via PAM (default) to start quickly. ## Configuration Set in `/etc/skd/env` (see `env.example`): @@ -36,12 +36,12 @@ Notes: - `./scripts/install.sh` will create `/etc/skd/env` from `env.example` if missing (edit afterwards) and ensure the `skd` service user/group exist. ## OIDC Setup -OIDC ist optional. Wenn der Provider noch nicht bereit ist, bleibe bei `SKD_AUTH_MODE=pam`. +OIDC ist optional. PAM bleibt immer verfuegbar; `SKD_AUTH_MODE` ist optional. 1. Issuer muss der externen URL des Providers entsprechen (TLS trust erforderlich). 2. OIDC Client registrieren (DCR), z.B.: ```bash export SKD_OIDC_ISSUER="https://auth.example.org" -export SKD_OIDC_REDIRECT_URI="https://[:port]/login/oidc/callback" +export SKD_OIDC_REDIRECT_URI="https:///login/oidc/callback" export OIDC_INITIAL_ACCESS_TOKEN="" ./scripts/register_oidc_client.sh ``` @@ -61,11 +61,11 @@ Hinweise: - Allowlist fuer OIDC: `SKD_AUTH_ALLOWED_USERS` prueft `preferred_username`, `email` oder `sub`. ## Running -- Service: managed by systemd; `./scripts/install.sh` writes the unit dynamically to `/etc/systemd/system/skd.service` with the current repo path and restarts it. -- Manual run: `./scripts/run.sh` (uses `.venv`, defaults to `0.0.0.0:8000`). -- Login (PAM): `curl -X POST -H "Content-Type: application/json" -d '{"username":"root","password":"..."}' http://localhost:8000/login` -- Login (OIDC): open `http://localhost:8000/login/oidc/start` β†’ provider β†’ redirected back with session cookie set. -- Health: `curl -H "Authorization: Bearer " http://localhost:8000/health` +- Service: managed by systemd; `./scripts/install.sh` writes the unit dynamically to `/etc/systemd/system/skd.service` with the current repo path and restarts it (runs as root for PAM). +- Manual run: `./scripts/run.sh` (uses `.venv`, defaults to `0.0.0.0:80`). +- Login (PAM): `curl -X POST -H "Content-Type: application/json" -d '{"username":"root","password":"..."}' http://localhost/login` +- Login (OIDC): open `http://localhost/login/oidc/start` β†’ provider β†’ redirected back with session cookie set. +- Health: `curl -H "Authorization: Bearer " http://localhost/health` ## OIDC Validation & Fallbacks - Validierungsschritte: `docs/oidc-validation.md` (State, Token-Exchange, Claims, Cookie). @@ -74,7 +74,7 @@ Hinweise: - Bei Self-Signed TLS: CA im System trusten oder in Dev PAM nutzen. ## API (Bearer token via `/login`) -- `GET /users` β†’ `[{user, logged_in}]` (manageable system users; excludes root) +- `GET /users` β†’ `[{user, logged_in, account_locked}]` (manageable system users; excludes root) - `POST /users/{name}/disable` with JSON `{countdown?, sound?, message?}` - `POST /users/{name}/enable` - `GET /health` @@ -82,11 +82,11 @@ Hinweise: Example: ```bash -token=$(curl -s -X POST -H "Content-Type: application/json" -d '{"username":"root","password":"..."}' http://localhost:8000/login | jq -r .token) +token=$(curl -s -X POST -H "Content-Type: application/json" -d '{"username":"root","password":"..."}' http://localhost/login | jq -r .token) curl -X POST -H "Authorization: Bearer $token" \ -H "Content-Type: application/json" \ -d '{"countdown":90,"sound":true}' \ - http://localhost:8000/users/child1/disable + http://localhost/users/child1/disable ``` ## Web UI diff --git a/backend/actions.py b/backend/actions.py index 70e921e..553c696 100644 --- a/backend/actions.py +++ b/backend/actions.py @@ -138,7 +138,6 @@ def disable_user( steps.append("sessions terminated") except subprocess.CalledProcessError as exc: if exc.returncode == 1: - # pkill returns 1 when no matching processes exist; not an error here. steps.append("no sessions to terminate") else: raise diff --git a/backend/app.py b/backend/app.py index 4a01eea..b8e83e9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -159,15 +159,15 @@ def users(settings: Settings = Depends(get_settings)) -> List[UserStatus]: dependencies=[Depends(get_current_admin)], ) def disable_user( - current_user: str = Depends(get_current_admin), username: str = Depends(validate_user), payload: ActionRequest | None = Body(default=None), settings: Settings = Depends(get_settings), + current_user: str = Depends(get_current_admin), ) -> ActionResponse: if username == current_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Refusing to disable the currently authenticated user", + detail="Cannot disable current user", ) try: steps = actions.disable_user( diff --git a/backend/auth.py b/backend/auth.py index 17e58c9..ec97bc5 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -2,7 +2,7 @@ import datetime as dt import grp import pwd import spwd -from typing import List, Set, Optional +from typing import List, Optional, Set import jwt import pam @@ -123,14 +123,9 @@ def list_manageable_users(settings: Settings) -> List[str]: def is_account_locked(username: str) -> bool: - """Return True when the account is locked in /etc/shadow.""" try: - shadow_entry = spwd.getspnam(username) - except KeyError: + entry = spwd.getspnam(username) + except (KeyError, PermissionError): return False - except PermissionError: - return False - shadow_password = shadow_entry.sp_pwdp or "" - if shadow_password in ("*", "!", "!!"): - return True - return shadow_password.startswith("!") + password_hash = entry.sp_pwdp or "" + return password_hash.startswith(("!", "*")) diff --git a/backend/templates/index.html b/backend/templates/index.html index 3e6e3f9..d355c44 100644 --- a/backend/templates/index.html +++ b/backend/templates/index.html @@ -154,9 +154,7 @@ statusDiv.textContent = 'Lade...'; try { const data = await api('/users'); - statusDiv.textContent = data - .map(u => `${u.user}: ${u.logged_in ? 'eingeloggt' : 'aus'} | ${u.account_locked ? 'deaktiviert' : 'aktiv'}`) - .join('\n') || 'Keine Daten'; + statusDiv.textContent = data.map(u => `${u.user}: ${u.account_locked ? 'deaktiviert' : 'aktiv'}, ${u.logged_in ? 'eingeloggt' : 'aus'}`).join('\n') || 'Keine Daten'; const select = document.getElementById('username'); select.innerHTML = ''; data.forEach(u => { diff --git a/docs/oidc-validation.md b/docs/oidc-validation.md index 98297c6..af3a460 100644 --- a/docs/oidc-validation.md +++ b/docs/oidc-validation.md @@ -7,7 +7,7 @@ By: Codex (GPT-5) Validiere den OIDC-Login-Flow gegen einen realen oder Stub-Provider und dokumentiere Ergebnisse. ## Preconditions -- Kiddo laeuft und ist erreichbar (z.B. `http://localhost:8000`). +- Kiddo laeuft und ist erreichbar (z.B. `http://localhost`). - OIDC Provider oder Stub erreichbar. - `SKD_AUTH_MODE=oidc` und `SKD_OIDC_*` gesetzt. - Redirect-URI: `https://[:port]/login/oidc/callback` ist registriert. @@ -29,7 +29,7 @@ Validiere den OIDC-Login-Flow gegen einen realen oder Stub-Provider und dokument - Datum: 28.12.2025 - Provider: nicht konfiguriert (IdP noch nicht bereit) - Host/Redirect: n/a -- Ergebnis: Blocked (Service nicht erreichbar unter http://localhost:8000/health) +- Ergebnis: Blocked (Service nicht erreichbar unter http://localhost/health) - Fehlerbilder: curl (7) Couldn't connect to server ## Fallbacks bei unvollstaendigem IdP diff --git a/env.example b/env.example index f5fe9bc..41120b9 100644 --- a/env.example +++ b/env.example @@ -1,6 +1,6 @@ # Copy to /etc/skd/env or .env for local runs # Optional allowlist of manageable users (otherwise all real users with uid>=1000) -SKD_ALLOWED_USERS=child1,child2 +SKD_ALLOWED_USERS= SKD_AUTH_SECRET=change-me-secret SKD_TOKEN_TTL_SECONDS=900 # PAM ist immer aktiv; OIDC wird zusaetzlich angeboten, wenn konfiguriert. @@ -13,7 +13,7 @@ SKD_AUTH_PAM_SERVICE=skd SKD_OIDC_ISSUER= SKD_OIDC_CLIENT_ID= SKD_OIDC_CLIENT_SECRET= -SKD_OIDC_REDIRECT_URI=http://localhost:8000/login/oidc/callback +SKD_OIDC_REDIRECT_URI=http://localhost/login/oidc/callback SKD_OIDC_SCOPES=openid profile email SKD_SESSION_COOKIE_NAME=skd_session SKD_SESSION_COOKIE_SECURE=false diff --git a/project-management/requirements/stories/US_000004.md b/project-management/requirements/stories/US_000004.md index beeafee..09d8c6f 100644 --- a/project-management/requirements/stories/US_000004.md +++ b/project-management/requirements/stories/US_000004.md @@ -10,7 +10,7 @@ Als Admin moechte ich verwaltbare Nutzer per API auflisten, damit ich ihren Logi ## Akzeptanzkriterien - Given ein autorisierter Admin-Login - When ein GET auf `/users` erfolgt -- Then die Antwort ist eine Liste von Eintraegen mit `user` und `logged_in` +- Then die Antwort ist eine Liste von Eintraegen mit `user`, `logged_in` und `account_locked` - And die Liste enthaelt nur verwaltbare, nicht-root Nutzer ## Task-Platzhalter diff --git a/project-management/requirements/stories/US_000005.md b/project-management/requirements/stories/US_000005.md index a75ea3d..37e0dc9 100644 --- a/project-management/requirements/stories/US_000005.md +++ b/project-management/requirements/stories/US_000005.md @@ -12,6 +12,7 @@ Als Admin moechte ich einen Nutzer per API deaktivieren, damit ich den Zugriff r - When ein POST auf `/users/{username}/disable` mit optionalen Feldern `countdown`, `sound`, `message` erfolgt - Then die Antwort enthaelt `user`, `action` = `disable`, `dry_run`, `steps` und `logged_in` - And nicht erlaubte Nutzer werden mit 403 abgewiesen +- And der aktuell angemeldete Admin kann sich nicht selbst deaktivieren ## Task-Platzhalter - TASK_000005: API disable action (Details bei Story-Start) diff --git a/project-management/requirements/stories/US_000012.md b/project-management/requirements/stories/US_000012.md index 6f0f626..54664f8 100644 --- a/project-management/requirements/stories/US_000012.md +++ b/project-management/requirements/stories/US_000012.md @@ -11,6 +11,7 @@ Als Operator moechte ich den Service lokal starten, damit ich die API ohne Syste - Given eine vorhandene `.venv` - When `scripts/run.sh` ausgefuehrt wird - Then `uvicorn` startet die App `backend.app:app` auf dem konfigurierten Host/Port +- And der Default-Port ist 80, wenn `PORT` nicht gesetzt ist ## Task-Platzhalter - TASK_000012: Run uvicorn service (Details bei Story-Start) diff --git a/project-management/requirements/stories/US_000013.md b/project-management/requirements/stories/US_000013.md index 587d175..dce506b 100644 --- a/project-management/requirements/stories/US_000013.md +++ b/project-management/requirements/stories/US_000013.md @@ -14,6 +14,7 @@ Als Operator moechte ich den Service installieren, damit er als Systemdienst lae - And das Projekt wird ins Install-Verzeichnis synchronisiert - And eine Env-Datei wird aus `env.example` erstellt, falls sie fehlt - And eine Systemd-Unit wird geschrieben und der Service gestartet +- And die Unit startet als root und lauscht auf Port 80 ## Task-Platzhalter - TASK_000013: Install service setup (Details bei Story-Start) diff --git a/project-management/requirements/stories/US_000017.md b/project-management/requirements/stories/US_000017.md index 024549c..fa6d7a8 100644 --- a/project-management/requirements/stories/US_000017.md +++ b/project-management/requirements/stories/US_000017.md @@ -11,6 +11,7 @@ Als Operator moechte ich eine Systemd-Unit im Repo haben, damit der Service stan - Given die Datei `systemd/skd.service` existiert - When die Unit inspiziert wird - Then sie enthaelt Description, User/Group, WorkingDirectory, EnvironmentFile, ExecStart und Restart-Policy +- And der Dienst laeuft als root und lauscht auf Port 80 ## Task-Platzhalter - TASK_000017: Systemd unit template (Details bei Story-Start) diff --git a/project-management/requirements/stories/US_000022.md b/project-management/requirements/stories/US_000022.md index 5056dbb..cba1c10 100644 --- a/project-management/requirements/stories/US_000022.md +++ b/project-management/requirements/stories/US_000022.md @@ -15,6 +15,7 @@ Als Admin moechte ich mich im Web-UI anmelden, Nutzer laden und Aktionen ausfueh - Then die Aktionsergebnisse (Steps/Status) werden als Text angezeigt - And Fehlerantworten werden als Text angezeigt - And die OIDC-Option ist deaktiviert, wenn keine Konfiguration vorliegt +- And der Status zeigt aktiv/deaktiviert pro Nutzer ## Task-Platzhalter - TASK_000022: UI login and actions (Details bei Story-Start) diff --git a/scripts/install.sh b/scripts/install.sh index 522f94e..5400797 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" SERVICE_NAME="${SERVICE_NAME:-skd}" -SERVICE_USER="${SERVICE_USER:-skd}" +SERVICE_USER="${SERVICE_USER:-root}" SERVICE_GROUP="${SERVICE_GROUP:-$SERVICE_USER}" INSTALL_DIR="${INSTALL_DIR:-/opt/sk}" ENV_DIR="/etc/${SERVICE_NAME}" @@ -109,11 +109,11 @@ After=network.target [Service] Type=simple -User=${SERVICE_USER} -Group=${SERVICE_GROUP} +User=root +Group=root WorkingDirectory=${INSTALL_DIR} EnvironmentFile=${ENV_FILE} -ExecStart=${INSTALL_DIR}/.venv/bin/uvicorn backend.app:app --host 0.0.0.0 --port 8000 +ExecStart=${INSTALL_DIR}/.venv/bin/uvicorn backend.app:app --host 0.0.0.0 --port 80 Restart=on-failure RestartSec=3 diff --git a/scripts/run.sh b/scripts/run.sh index c570239..991bb69 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -9,6 +9,6 @@ source .venv/bin/activate UVICORN_CMD=${UVICORN_CMD:-uvicorn} HOST=${HOST:-0.0.0.0} -PORT=${PORT:-8000} +PORT=${PORT:-80} exec "${UVICORN_CMD}" backend.app:app --host "${HOST}" --port "${PORT}" diff --git a/systemd/skd.service b/systemd/skd.service index 628c74c..f77d657 100644 --- a/systemd/skd.service +++ b/systemd/skd.service @@ -4,11 +4,11 @@ After=network.target [Service] Type=simple -User=skd -Group=skd +User=root +Group=root WorkingDirectory=/opt/sk EnvironmentFile=/etc/skd/env -ExecStart=/opt/sk/.venv/bin/uvicorn backend.app:app --host 0.0.0.0 --port 8000 +ExecStart=/opt/sk/.venv/bin/uvicorn backend.app:app --host 0.0.0.0 --port 80 Restart=on-failure RestartSec=3