feature: root service and account status

This commit is contained in:
2025-12-28 22:31:56 +01:00
parent b566055d0a
commit 2d1ca05bf0
18 changed files with 39 additions and 41 deletions

View File

@ -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: 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 | 🏗️ 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: 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 ## Legende

View File

@ -8,7 +8,7 @@ ENV_FILE ?= $(ENV_DIR)/env
SYSTEMD_PATH ?= /etc/systemd/system/$(SERVICE).service SYSTEMD_PATH ?= /etc/systemd/system/$(SERVICE).service
BRANCH ?= main BRANCH ?= main
HOST ?= 127.0.0.1 HOST ?= 127.0.0.1
PORT ?= 8000 PORT ?= 80
HEALTH_URL ?= http://$(HOST):$(PORT)/health HEALTH_URL ?= http://$(HOST):$(PORT)/health
TOKEN ?= $(shell awk -F= '/^SKD_AUTH_TOKEN=/{print $$2}' $(ENV_FILE) 2>/dev/null) TOKEN ?= $(shell awk -F= '/^SKD_AUTH_TOKEN=/{print $$2}' $(ENV_FILE) 2>/dev/null)
KEEP_INSTALL_DIR ?= 1 KEEP_INSTALL_DIR ?= 1

View File

@ -16,7 +16,7 @@ cd /opt/sk
./scripts/install.sh ./scripts/install.sh
sudo systemctl status skd.service 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 ## Configuration
Set in `/etc/skd/env` (see `env.example`): 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. - `./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 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). 1. Issuer muss der externen URL des Providers entsprechen (TLS trust erforderlich).
2. OIDC Client registrieren (DCR), z.B.: 2. OIDC Client registrieren (DCR), z.B.:
```bash ```bash
export SKD_OIDC_ISSUER="https://auth.example.org" export SKD_OIDC_ISSUER="https://auth.example.org"
export SKD_OIDC_REDIRECT_URI="https://<device-host>[:port]/login/oidc/callback" export SKD_OIDC_REDIRECT_URI="https://<device-host>/login/oidc/callback"
export OIDC_INITIAL_ACCESS_TOKEN="<initial-access-token>" export OIDC_INITIAL_ACCESS_TOKEN="<initial-access-token>"
./scripts/register_oidc_client.sh ./scripts/register_oidc_client.sh
``` ```
@ -61,11 +61,11 @@ Hinweise:
- Allowlist fuer OIDC: `SKD_AUTH_ALLOWED_USERS` prueft `preferred_username`, `email` oder `sub`. - Allowlist fuer OIDC: `SKD_AUTH_ALLOWED_USERS` prueft `preferred_username`, `email` oder `sub`.
## Running ## 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. - 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:8000`). - 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:8000/login` - Login (PAM): `curl -X POST -H "Content-Type: application/json" -d '{"username":"root","password":"..."}' http://localhost/login`
- Login (OIDC): open `http://localhost:8000/login/oidc/start` → provider → redirected back with session cookie set. - Login (OIDC): open `http://localhost/login/oidc/start` → provider → redirected back with session cookie set.
- Health: `curl -H "Authorization: Bearer <token>" http://localhost:8000/health` - Health: `curl -H "Authorization: Bearer <token>" http://localhost/health`
## OIDC Validation & Fallbacks ## OIDC Validation & Fallbacks
- Validierungsschritte: `docs/oidc-validation.md` (State, Token-Exchange, Claims, Cookie). - 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. - Bei Self-Signed TLS: CA im System trusten oder in Dev PAM nutzen.
## API (Bearer token via `/login`) ## 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}/disable` with JSON `{countdown?, sound?, message?}`
- `POST /users/{name}/enable` - `POST /users/{name}/enable`
- `GET /health` - `GET /health`
@ -82,11 +82,11 @@ Hinweise:
Example: Example:
```bash ```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" \ curl -X POST -H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"countdown":90,"sound":true}' \ -d '{"countdown":90,"sound":true}' \
http://localhost:8000/users/child1/disable http://localhost/users/child1/disable
``` ```
## Web UI ## Web UI

View File

@ -138,7 +138,6 @@ def disable_user(
steps.append("sessions terminated") steps.append("sessions terminated")
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
if exc.returncode == 1: if exc.returncode == 1:
# pkill returns 1 when no matching processes exist; not an error here.
steps.append("no sessions to terminate") steps.append("no sessions to terminate")
else: else:
raise raise

View File

@ -159,15 +159,15 @@ def users(settings: Settings = Depends(get_settings)) -> List[UserStatus]:
dependencies=[Depends(get_current_admin)], dependencies=[Depends(get_current_admin)],
) )
def disable_user( def disable_user(
current_user: str = Depends(get_current_admin),
username: str = Depends(validate_user), username: str = Depends(validate_user),
payload: ActionRequest | None = Body(default=None), payload: ActionRequest | None = Body(default=None),
settings: Settings = Depends(get_settings), settings: Settings = Depends(get_settings),
current_user: str = Depends(get_current_admin),
) -> ActionResponse: ) -> ActionResponse:
if username == current_user: if username == current_user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Refusing to disable the currently authenticated user", detail="Cannot disable current user",
) )
try: try:
steps = actions.disable_user( steps = actions.disable_user(

View File

@ -2,7 +2,7 @@ import datetime as dt
import grp import grp
import pwd import pwd
import spwd import spwd
from typing import List, Set, Optional from typing import List, Optional, Set
import jwt import jwt
import pam import pam
@ -123,14 +123,9 @@ def list_manageable_users(settings: Settings) -> List[str]:
def is_account_locked(username: str) -> bool: def is_account_locked(username: str) -> bool:
"""Return True when the account is locked in /etc/shadow."""
try: try:
shadow_entry = spwd.getspnam(username) entry = spwd.getspnam(username)
except KeyError: except (KeyError, PermissionError):
return False return False
except PermissionError: password_hash = entry.sp_pwdp or ""
return False return password_hash.startswith(("!", "*"))
shadow_password = shadow_entry.sp_pwdp or ""
if shadow_password in ("*", "!", "!!"):
return True
return shadow_password.startswith("!")

View File

@ -154,9 +154,7 @@
statusDiv.textContent = 'Lade...'; statusDiv.textContent = 'Lade...';
try { try {
const data = await api('/users'); const data = await api('/users');
statusDiv.textContent = data statusDiv.textContent = data.map(u => `${u.user}: ${u.account_locked ? 'deaktiviert' : 'aktiv'}, ${u.logged_in ? 'eingeloggt' : 'aus'}`).join('\n') || 'Keine Daten';
.map(u => `${u.user}: ${u.logged_in ? 'eingeloggt' : 'aus'} | ${u.account_locked ? 'deaktiviert' : 'aktiv'}`)
.join('\n') || 'Keine Daten';
const select = document.getElementById('username'); const select = document.getElementById('username');
select.innerHTML = '<option value="">-- wählen --</option>'; select.innerHTML = '<option value="">-- wählen --</option>';
data.forEach(u => { data.forEach(u => {

View File

@ -7,7 +7,7 @@ By: Codex (GPT-5)
Validiere den OIDC-Login-Flow gegen einen realen oder Stub-Provider und dokumentiere Ergebnisse. Validiere den OIDC-Login-Flow gegen einen realen oder Stub-Provider und dokumentiere Ergebnisse.
## Preconditions ## 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. - OIDC Provider oder Stub erreichbar.
- `SKD_AUTH_MODE=oidc` und `SKD_OIDC_*` gesetzt. - `SKD_AUTH_MODE=oidc` und `SKD_OIDC_*` gesetzt.
- Redirect-URI: `https://<device-host>[:port]/login/oidc/callback` ist registriert. - Redirect-URI: `https://<device-host>[: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 - Datum: 28.12.2025
- Provider: nicht konfiguriert (IdP noch nicht bereit) - Provider: nicht konfiguriert (IdP noch nicht bereit)
- Host/Redirect: n/a - 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 - Fehlerbilder: curl (7) Couldn't connect to server
## Fallbacks bei unvollstaendigem IdP ## Fallbacks bei unvollstaendigem IdP

View File

@ -1,6 +1,6 @@
# Copy to /etc/skd/env or .env for local runs # Copy to /etc/skd/env or .env for local runs
# Optional allowlist of manageable users (otherwise all real users with uid>=1000) # 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_AUTH_SECRET=change-me-secret
SKD_TOKEN_TTL_SECONDS=900 SKD_TOKEN_TTL_SECONDS=900
# PAM ist immer aktiv; OIDC wird zusaetzlich angeboten, wenn konfiguriert. # PAM ist immer aktiv; OIDC wird zusaetzlich angeboten, wenn konfiguriert.
@ -13,7 +13,7 @@ SKD_AUTH_PAM_SERVICE=skd
SKD_OIDC_ISSUER= SKD_OIDC_ISSUER=
SKD_OIDC_CLIENT_ID= SKD_OIDC_CLIENT_ID=
SKD_OIDC_CLIENT_SECRET= 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_OIDC_SCOPES=openid profile email
SKD_SESSION_COOKIE_NAME=skd_session SKD_SESSION_COOKIE_NAME=skd_session
SKD_SESSION_COOKIE_SECURE=false SKD_SESSION_COOKIE_SECURE=false

View File

@ -10,7 +10,7 @@ Als Admin moechte ich verwaltbare Nutzer per API auflisten, damit ich ihren Logi
## Akzeptanzkriterien ## Akzeptanzkriterien
- Given ein autorisierter Admin-Login - Given ein autorisierter Admin-Login
- When ein GET auf `/users` erfolgt - 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 - And die Liste enthaelt nur verwaltbare, nicht-root Nutzer
## Task-Platzhalter ## Task-Platzhalter

View File

@ -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 - 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` - Then die Antwort enthaelt `user`, `action` = `disable`, `dry_run`, `steps` und `logged_in`
- And nicht erlaubte Nutzer werden mit 403 abgewiesen - And nicht erlaubte Nutzer werden mit 403 abgewiesen
- And der aktuell angemeldete Admin kann sich nicht selbst deaktivieren
## Task-Platzhalter ## Task-Platzhalter
- TASK_000005: API disable action (Details bei Story-Start) - TASK_000005: API disable action (Details bei Story-Start)

View File

@ -11,6 +11,7 @@ Als Operator moechte ich den Service lokal starten, damit ich die API ohne Syste
- Given eine vorhandene `.venv` - Given eine vorhandene `.venv`
- When `scripts/run.sh` ausgefuehrt wird - When `scripts/run.sh` ausgefuehrt wird
- Then `uvicorn` startet die App `backend.app:app` auf dem konfigurierten Host/Port - 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-Platzhalter
- TASK_000012: Run uvicorn service (Details bei Story-Start) - TASK_000012: Run uvicorn service (Details bei Story-Start)

View File

@ -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 das Projekt wird ins Install-Verzeichnis synchronisiert
- And eine Env-Datei wird aus `env.example` erstellt, falls sie fehlt - And eine Env-Datei wird aus `env.example` erstellt, falls sie fehlt
- And eine Systemd-Unit wird geschrieben und der Service gestartet - And eine Systemd-Unit wird geschrieben und der Service gestartet
- And die Unit startet als root und lauscht auf Port 80
## Task-Platzhalter ## Task-Platzhalter
- TASK_000013: Install service setup (Details bei Story-Start) - TASK_000013: Install service setup (Details bei Story-Start)

View File

@ -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 - Given die Datei `systemd/skd.service` existiert
- When die Unit inspiziert wird - When die Unit inspiziert wird
- Then sie enthaelt Description, User/Group, WorkingDirectory, EnvironmentFile, ExecStart und Restart-Policy - 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-Platzhalter
- TASK_000017: Systemd unit template (Details bei Story-Start) - TASK_000017: Systemd unit template (Details bei Story-Start)

View File

@ -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 - Then die Aktionsergebnisse (Steps/Status) werden als Text angezeigt
- And Fehlerantworten werden als Text angezeigt - And Fehlerantworten werden als Text angezeigt
- And die OIDC-Option ist deaktiviert, wenn keine Konfiguration vorliegt - And die OIDC-Option ist deaktiviert, wenn keine Konfiguration vorliegt
- And der Status zeigt aktiv/deaktiviert pro Nutzer
## Task-Platzhalter ## Task-Platzhalter
- TASK_000022: UI login and actions (Details bei Story-Start) - TASK_000022: UI login and actions (Details bei Story-Start)

View File

@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" SOURCE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
SERVICE_NAME="${SERVICE_NAME:-skd}" SERVICE_NAME="${SERVICE_NAME:-skd}"
SERVICE_USER="${SERVICE_USER:-skd}" SERVICE_USER="${SERVICE_USER:-root}"
SERVICE_GROUP="${SERVICE_GROUP:-$SERVICE_USER}" SERVICE_GROUP="${SERVICE_GROUP:-$SERVICE_USER}"
INSTALL_DIR="${INSTALL_DIR:-/opt/sk}" INSTALL_DIR="${INSTALL_DIR:-/opt/sk}"
ENV_DIR="/etc/${SERVICE_NAME}" ENV_DIR="/etc/${SERVICE_NAME}"
@ -109,11 +109,11 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
User=${SERVICE_USER} User=root
Group=${SERVICE_GROUP} Group=root
WorkingDirectory=${INSTALL_DIR} WorkingDirectory=${INSTALL_DIR}
EnvironmentFile=${ENV_FILE} 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 Restart=on-failure
RestartSec=3 RestartSec=3

View File

@ -9,6 +9,6 @@ source .venv/bin/activate
UVICORN_CMD=${UVICORN_CMD:-uvicorn} UVICORN_CMD=${UVICORN_CMD:-uvicorn}
HOST=${HOST:-0.0.0.0} HOST=${HOST:-0.0.0.0}
PORT=${PORT:-8000} PORT=${PORT:-80}
exec "${UVICORN_CMD}" backend.app:app --host "${HOST}" --port "${PORT}" exec "${UVICORN_CMD}" backend.app:app --host "${HOST}" --port "${PORT}"

View File

@ -4,11 +4,11 @@ After=network.target
[Service] [Service]
Type=simple Type=simple
User=skd User=root
Group=skd Group=root
WorkingDirectory=/opt/sk WorkingDirectory=/opt/sk
EnvironmentFile=/etc/skd/env 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 Restart=on-failure
RestartSec=3 RestartSec=3