code: keep pam enabled and gate oidc

This commit is contained in:
2025-12-28 13:49:30 +01:00
parent 97ea7070cc
commit e380288755
4 changed files with 41 additions and 17 deletions

View File

@ -29,10 +29,10 @@ templates = Jinja2Templates(directory="backend/templates")
def get_oidc_client(settings: Settings = Depends(get_settings)) -> OIDCClient: def get_oidc_client(settings: Settings = Depends(get_settings)) -> OIDCClient:
if settings.auth_mode != "oidc": if not settings.oidc_enabled:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="OIDC auth not enabled", detail="OIDC not configured",
) )
try: try:
return OIDCClient(settings) return OIDCClient(settings)
@ -59,7 +59,8 @@ def whoami(
current_user: str = Depends(get_current_admin), current_user: str = Depends(get_current_admin),
settings: Settings = Depends(get_settings), settings: Settings = Depends(get_settings),
) -> dict: ) -> 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) @app.post("/login", response_model=LoginResponse)
@ -115,7 +116,7 @@ def oidc_callback(
username = oidc.extract_username(claims) username = oidc.extract_username(claims)
if not username: if not username:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing username claim") 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") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User not authorized to log in")
token = issue_token(username, settings) token = issue_token(username, settings)
@ -132,6 +133,11 @@ def oidc_callback(
return redirect 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)]) @app.get("/users", response_model=List[UserStatus], dependencies=[Depends(get_current_admin)])
def users(settings: Settings = Depends(get_settings)) -> List[UserStatus]: def users(settings: Settings = Depends(get_settings)) -> List[UserStatus]:
logged_in = set(actions.list_logged_in_users()) logged_in = set(actions.list_logged_in_users())

View File

@ -1,7 +1,7 @@
import datetime as dt import datetime as dt
import grp import grp
import pwd import pwd
from typing import List, Set from typing import List, Set, Optional
import jwt import jwt
import pam import pam
@ -23,8 +23,8 @@ def _is_member_of(username: str, groups: Set[str]) -> bool:
return bool(user_groups & groups) return bool(user_groups & groups)
def is_authorized_admin(username: str, settings: Settings) -> bool: def is_authorized_admin(username: str, settings: Settings, mode: Optional[str] = None) -> bool:
if settings.auth_mode == "oidc": if mode == "oidc":
allowed_users = set(settings.auth_allowed_users) allowed_users = set(settings.auth_allowed_users)
if allowed_users and username not in allowed_users: if allowed_users and username not in allowed_users:
return False 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: def authenticate_admin_user(username: str, password: str, settings: Settings) -> None:
if settings.auth_mode != "pam": if not is_authorized_admin(username, settings, 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):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="User not authorized to log in", 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 raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") from exc
username = payload.get("sub") 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") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorized user")
return username return username

View File

@ -32,6 +32,9 @@ class Settings:
self.oidc_state_cookie_name: str = os.getenv( self.oidc_state_cookie_name: str = os.getenv(
"SKD_OIDC_STATE_COOKIE_NAME", "skd_oidc_state" "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_countdown: int = int(os.getenv("SKD_DEFAULT_COUNTDOWN", "60"))
self.default_sound: bool = os.getenv("SKD_DEFAULT_SOUND", "false").lower() == "true" self.default_sound: bool = os.getenv("SKD_DEFAULT_SOUND", "false").lower() == "true"
self.notify_timeout: int = int(os.getenv("SKD_NOTIFY_TIMEOUT", "5")) self.notify_timeout: int = int(os.getenv("SKD_NOTIFY_TIMEOUT", "5"))

View File

@ -18,11 +18,11 @@
</header> </header>
<section> <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> <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> <button id="oidcLogin" type="button">Login via OIDC</button>
<hr /> <hr />
<p>Lokale Anmeldung (PAM) nur falls OIDC nicht verfügbar:</p> <p>Lokale Anmeldung (PAM) ist immer moeglich:</p>
<form id="loginForm"> <form id="loginForm">
<div class="grid"> <div class="grid">
<div> <div>
@ -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() { async function refreshUsers() {
statusDiv.textContent = 'Lade...'; statusDiv.textContent = 'Lade...';
try { try {
@ -206,6 +220,7 @@
}); });
checkSession(); checkSession();
checkOidcStatus();
</script> </script>
</body> </body>
</html> </html>