code: keep pam enabled and gate oidc
This commit is contained in:
@ -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())
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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"))
|
||||||
|
|||||||
@ -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>
|
||||||
@ -84,7 +84,7 @@
|
|||||||
<div id="result" class="log"></div>
|
<div id="result" class="log"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const statusDiv = document.getElementById('status');
|
const statusDiv = document.getElementById('status');
|
||||||
const resultDiv = document.getElementById('result');
|
const resultDiv = document.getElementById('result');
|
||||||
const loginStatus = document.getElementById('loginStatus');
|
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() {
|
async function refreshUsers() {
|
||||||
statusDiv.textContent = 'Lade...';
|
statusDiv.textContent = 'Lade...';
|
||||||
try {
|
try {
|
||||||
@ -206,6 +220,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
checkSession();
|
checkSession();
|
||||||
|
checkOidcStatus();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user