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: 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

View File

@ -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

View File

@ -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://<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>"
./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 <token>" 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 <token>" 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

View File

@ -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

View File

@ -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(

View File

@ -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(("!", "*"))

View File

@ -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 = '<option value="">-- wählen --</option>';
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.
## 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://<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
- 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

View File

@ -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

View File

@ -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

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
- 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)

View File

@ -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)

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 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)

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
- 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)

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
- 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)

View File

@ -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

View File

@ -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}"

View File

@ -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