Compare commits
2 Commits
4809027227
...
e380288755
| Author | SHA1 | Date | |
|---|---|---|---|
| e380288755 | |||
| 97ea7070cc |
@ -14,6 +14,7 @@ By: Codex (GPT-5)
|
||||
| 28.12.2025 | 📝 Req | ID: US_000025 Runbook/Validierungsschritte dokumentiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 📝 Req | ID: US_000025 Validation blocked (Service/IdP nicht bereit). 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) |
|
||||
|
||||
---
|
||||
## Legende
|
||||
|
||||
@ -20,7 +20,7 @@ Then open `http://localhost:8000/` and log in via PAM (default) to start quickly
|
||||
|
||||
## Configuration
|
||||
Set in `/etc/skd/env` (see `env.example`):
|
||||
- `SKD_AUTH_MODE`: `pam` (default) or `oidc`.
|
||||
- PAM-Login ist immer aktiv. OIDC wird zusaetzlich angeboten, wenn konfiguriert.
|
||||
- `SKD_AUTH_SECRET`: HMAC secret for bearer tokens/cookies (set a strong value).
|
||||
- `SKD_TOKEN_TTL_SECONDS`: token lifetime (default 900s).
|
||||
- `SKD_AUTH_ALLOWED_USERS`: optional comma list of accounts allowed to log in (used for PAM and as an allowlist for OIDC claims).
|
||||
|
||||
@ -29,10 +29,10 @@ templates = Jinja2Templates(directory="backend/templates")
|
||||
|
||||
|
||||
def get_oidc_client(settings: Settings = Depends(get_settings)) -> OIDCClient:
|
||||
if settings.auth_mode != "oidc":
|
||||
if not settings.oidc_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="OIDC auth not enabled",
|
||||
detail="OIDC not configured",
|
||||
)
|
||||
try:
|
||||
return OIDCClient(settings)
|
||||
@ -59,7 +59,8 @@ def whoami(
|
||||
current_user: str = Depends(get_current_admin),
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> dict:
|
||||
return {"user": current_user, "auth_mode": settings.auth_mode}
|
||||
auth_mode = "pam+oidc" if settings.oidc_enabled else "pam"
|
||||
return {"user": current_user, "auth_mode": auth_mode}
|
||||
|
||||
|
||||
@app.post("/login", response_model=LoginResponse)
|
||||
@ -115,7 +116,7 @@ def oidc_callback(
|
||||
username = oidc.extract_username(claims)
|
||||
if not username:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing username claim")
|
||||
if not is_authorized_admin(username, settings):
|
||||
if not is_authorized_admin(username, settings, mode="oidc"):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User not authorized to log in")
|
||||
|
||||
token = issue_token(username, settings)
|
||||
@ -132,6 +133,11 @@ def oidc_callback(
|
||||
return redirect
|
||||
|
||||
|
||||
@app.get("/login/oidc/status")
|
||||
def oidc_status(settings: Settings = Depends(get_settings)) -> dict:
|
||||
return {"enabled": settings.oidc_enabled}
|
||||
|
||||
|
||||
@app.get("/users", response_model=List[UserStatus], dependencies=[Depends(get_current_admin)])
|
||||
def users(settings: Settings = Depends(get_settings)) -> List[UserStatus]:
|
||||
logged_in = set(actions.list_logged_in_users())
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import datetime as dt
|
||||
import grp
|
||||
import pwd
|
||||
from typing import List, Set
|
||||
from typing import List, Set, Optional
|
||||
|
||||
import jwt
|
||||
import pam
|
||||
@ -23,8 +23,8 @@ def _is_member_of(username: str, groups: Set[str]) -> bool:
|
||||
return bool(user_groups & groups)
|
||||
|
||||
|
||||
def is_authorized_admin(username: str, settings: Settings) -> bool:
|
||||
if settings.auth_mode == "oidc":
|
||||
def is_authorized_admin(username: str, settings: Settings, mode: Optional[str] = None) -> bool:
|
||||
if mode == "oidc":
|
||||
allowed_users = set(settings.auth_allowed_users)
|
||||
if allowed_users and username not in allowed_users:
|
||||
return False
|
||||
@ -46,12 +46,7 @@ def is_authorized_admin(username: str, settings: Settings) -> bool:
|
||||
|
||||
|
||||
def authenticate_admin_user(username: str, password: str, settings: Settings) -> None:
|
||||
if settings.auth_mode != "pam":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Password login disabled; OIDC is configured",
|
||||
)
|
||||
if not is_authorized_admin(username, settings):
|
||||
if not is_authorized_admin(username, settings, mode="pam"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User not authorized to log in",
|
||||
@ -82,7 +77,12 @@ def decode_token(token: str, settings: Settings) -> str:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") from exc
|
||||
|
||||
username = payload.get("sub")
|
||||
if not username or not is_authorized_admin(username, settings):
|
||||
if not username:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
||||
if not (
|
||||
is_authorized_admin(username, settings, mode="pam")
|
||||
or is_authorized_admin(username, settings, mode="oidc")
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorized user")
|
||||
return username
|
||||
|
||||
|
||||
@ -32,6 +32,9 @@ class Settings:
|
||||
self.oidc_state_cookie_name: str = os.getenv(
|
||||
"SKD_OIDC_STATE_COOKIE_NAME", "skd_oidc_state"
|
||||
)
|
||||
self.oidc_enabled: bool = bool(
|
||||
self.oidc_issuer and self.oidc_client_id and self.oidc_client_secret
|
||||
)
|
||||
self.default_countdown: int = int(os.getenv("SKD_DEFAULT_COUNTDOWN", "60"))
|
||||
self.default_sound: bool = os.getenv("SKD_DEFAULT_SOUND", "false").lower() == "true"
|
||||
self.notify_timeout: int = int(os.getenv("SKD_NOTIFY_TIMEOUT", "5"))
|
||||
|
||||
@ -18,11 +18,11 @@
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h3>Login (nur Root-User)</h3>
|
||||
<h3>Login</h3>
|
||||
<p>Bevorzugt OIDC nutzen, falls konfiguriert. Die Anmeldung öffnet den Identity Provider und setzt eine Session-Cookie.</p>
|
||||
<button id="oidcLogin" type="button">Login via OIDC</button>
|
||||
<hr />
|
||||
<p>Lokale Anmeldung (PAM) nur falls OIDC nicht verfügbar:</p>
|
||||
<p>Lokale Anmeldung (PAM) ist immer moeglich:</p>
|
||||
<form id="loginForm">
|
||||
<div class="grid">
|
||||
<div>
|
||||
@ -84,7 +84,7 @@
|
||||
<div id="result" class="log"></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
<script>
|
||||
const statusDiv = document.getElementById('status');
|
||||
const resultDiv = document.getElementById('result');
|
||||
const loginStatus = document.getElementById('loginStatus');
|
||||
@ -136,6 +136,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function checkOidcStatus() {
|
||||
const button = document.getElementById('oidcLogin');
|
||||
try {
|
||||
const data = await api('/login/oidc/status');
|
||||
if (!data.enabled) {
|
||||
button.disabled = true;
|
||||
button.title = 'OIDC nicht konfiguriert';
|
||||
}
|
||||
} catch (err) {
|
||||
button.disabled = true;
|
||||
button.title = 'OIDC-Status nicht erreichbar';
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshUsers() {
|
||||
statusDiv.textContent = 'Lade...';
|
||||
try {
|
||||
@ -206,6 +220,7 @@
|
||||
});
|
||||
|
||||
checkSession();
|
||||
checkOidcStatus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
SKD_ALLOWED_USERS=child1,child2
|
||||
SKD_AUTH_SECRET=change-me-secret
|
||||
SKD_TOKEN_TTL_SECONDS=900
|
||||
# Auth mode: pam (default) or oidc
|
||||
# PAM ist immer aktiv; OIDC wird zusaetzlich angeboten, wenn konfiguriert.
|
||||
# SKD_AUTH_MODE bleibt optional und wird derzeit nicht erzwungen.
|
||||
SKD_AUTH_MODE=pam
|
||||
SKD_AUTH_ALLOWED_USERS=
|
||||
SKD_AUTH_ALLOWED_GROUPS=sudo
|
||||
|
||||
@ -13,6 +13,7 @@ Als Admin moechte ich mich per PAM-Login anmelden, damit ich ein Session-Token e
|
||||
- Then die Antwort enthaelt `token` und `expires_in`
|
||||
- And ein Session-Cookie mit dem Token wird gesetzt
|
||||
- And der Login ist ohne OIDC-Konfiguration als Schnellstart moeglich
|
||||
- And PAM-Login bleibt auch bei aktivem OIDC verfuegbar
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000007: PAM login token (Details bei Story-Start)
|
||||
|
||||
@ -8,12 +8,13 @@ Status: Done
|
||||
Als Admin moechte ich mich per OIDC anmelden, damit ich ohne Passwort-Login zugreifen kann.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- Given `SKD_AUTH_MODE=oidc` und ein erreichbarer OIDC-Provider
|
||||
- Given OIDC ist konfiguriert und ein erreichbarer OIDC-Provider
|
||||
- When ein GET auf `/login/oidc/start` erfolgt
|
||||
- Then der Nutzer wird zum Provider umgeleitet und ein State-Cookie gesetzt
|
||||
- When der Provider auf `/login/oidc/callback` mit Code und State zurueckleitet
|
||||
- Then der State wird validiert und ein Session-Cookie gesetzt
|
||||
- And bei ungueltigem State erfolgt eine 400-Antwort
|
||||
- And die OIDC-Option wird deaktiviert, wenn keine OIDC-Konfiguration vorliegt
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000008: OIDC auth callback (Details bei Story-Start)
|
||||
|
||||
@ -10,7 +10,7 @@ Als Operator moechte ich Konfigurationen per ENV setzen, damit Verhalten und Def
|
||||
## Akzeptanzkriterien
|
||||
- Given Umgebungsvariablen aus `env.example`
|
||||
- When der Service startet
|
||||
- Then Auth- und Session-Settings werden aus ENV geladen (`SKD_AUTH_MODE`, `SKD_AUTH_SECRET`, `SKD_TOKEN_TTL_SECONDS`, `SKD_AUTH_ALLOWED_USERS`, `SKD_AUTH_ALLOWED_GROUPS`, `SKD_AUTH_PAM_SERVICE`, `SKD_SESSION_COOKIE_NAME`, `SKD_SESSION_COOKIE_SECURE`, `SKD_OIDC_STATE_COOKIE_NAME`)
|
||||
- Then Auth- und Session-Settings werden aus ENV geladen (`SKD_AUTH_SECRET`, `SKD_TOKEN_TTL_SECONDS`, `SKD_AUTH_ALLOWED_USERS`, `SKD_AUTH_ALLOWED_GROUPS`, `SKD_AUTH_PAM_SERVICE`, `SKD_SESSION_COOKIE_NAME`, `SKD_SESSION_COOKIE_SECURE`, `SKD_OIDC_STATE_COOKIE_NAME`)
|
||||
- And OIDC-Settings werden aus ENV geladen (`SKD_OIDC_ISSUER`, `SKD_OIDC_CLIENT_ID`, `SKD_OIDC_CLIENT_SECRET`, `SKD_OIDC_REDIRECT_URI`, `SKD_OIDC_SCOPES`)
|
||||
- And Allowlist/Defaults werden aus ENV geladen (`SKD_ALLOWED_USERS`, `SKD_DEFAULT_COUNTDOWN`, `SKD_DEFAULT_SOUND`, `SKD_NOTIFY_TIMEOUT`, `SKD_DRY_RUN`)
|
||||
- And Sound/Notify-Pfade sind ueber ENV ueberschreibbar (`SKD_SOUND_PLAYER`, `SKD_SOUND_FILE`, `SKD_NOTIFY_SEND_PATH`)
|
||||
|
||||
@ -14,6 +14,7 @@ Als Admin moechte ich mich im Web-UI anmelden, Nutzer laden und Aktionen ausfueh
|
||||
- When ich Nutzer lade und eine Aktion sende
|
||||
- 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
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000022: UI login and actions (Details bei Story-Start)
|
||||
|
||||
Reference in New Issue
Block a user