feature: root service and account status
This commit is contained in:
@ -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
|
||||
|
||||
2
Makefile
2
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
|
||||
|
||||
22
README.md
22
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://<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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(("!", "*"))
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user