auth providers
This commit is contained in:
@@ -9,7 +9,17 @@ from urllib.parse import urlparse
|
||||
from flask import abort, g, jsonify, redirect, request, session, url_for
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
from ..config import AUTH_ENABLE
|
||||
from ..config import (
|
||||
AUTH_ENABLE,
|
||||
AUTH_PROVIDER,
|
||||
AUTH_PROXY_AUTO_CREATE,
|
||||
AUTH_PROXY_DEFAULT_ACCESS,
|
||||
AUTH_PROXY_DEFAULT_ROLE,
|
||||
AUTH_PROXY_EMAIL_HEADER,
|
||||
AUTH_PROXY_NAME_HEADER,
|
||||
AUTH_PROXY_SUBJECT_HEADER,
|
||||
AUTH_PROXY_USER_HEADER,
|
||||
)
|
||||
from ..db import connect, default_user_id, utcnow
|
||||
|
||||
PUBLIC_ENDPOINTS = {"main.login", "main.logout", "api.auth_login", "api.auth_me", "static"}
|
||||
@@ -47,6 +57,14 @@ def enabled() -> bool:
|
||||
return bool(AUTH_ENABLE)
|
||||
|
||||
|
||||
def provider() -> str:
|
||||
return AUTH_PROVIDER if AUTH_PROVIDER in {"local", "proxy", "tinyauth"} else "local"
|
||||
|
||||
|
||||
def uses_external_provider() -> bool:
|
||||
return enabled() and provider() in {"proxy", "tinyauth"}
|
||||
|
||||
|
||||
def password_hash(password: str) -> str:
|
||||
return generate_password_hash(password or "")
|
||||
|
||||
@@ -57,6 +75,9 @@ def current_user_id() -> int:
|
||||
api_user_id = getattr(g, "api_user_id", None)
|
||||
if api_user_id:
|
||||
return int(api_user_id)
|
||||
external_user_id = getattr(g, "external_user_id", None)
|
||||
if external_user_id:
|
||||
return int(external_user_id)
|
||||
try:
|
||||
return int(session.get("user_id") or 0)
|
||||
except Exception:
|
||||
@@ -69,7 +90,7 @@ def current_user() -> dict[str, Any] | None:
|
||||
return None
|
||||
with connect() as conn:
|
||||
return conn.execute(
|
||||
"SELECT id, username, role, is_active, created_at, updated_at FROM users WHERE id=?",
|
||||
"SELECT id, username, email, display_name, external_auth_provider, external_subject, role, is_active, created_at, updated_at FROM users WHERE id=?",
|
||||
(uid,),
|
||||
).fetchone()
|
||||
|
||||
@@ -200,6 +221,8 @@ def require_profile_write(profile_id: int | None) -> None:
|
||||
def login_user(username: str, password: str) -> dict[str, Any] | None:
|
||||
if not enabled():
|
||||
return {"id": default_user_id(), "username": "default", "role": "admin", "is_active": 1}
|
||||
if uses_external_provider():
|
||||
return None
|
||||
with connect() as conn:
|
||||
user = conn.execute("SELECT * FROM users WHERE username=?", (username.strip(),)).fetchone()
|
||||
if not user or not int(user.get("is_active") or 0):
|
||||
@@ -213,6 +236,109 @@ def login_user(username: str, password: str) -> dict[str, Any] | None:
|
||||
return current_user()
|
||||
|
||||
|
||||
|
||||
|
||||
def _clean_header_value(name: str) -> str:
|
||||
if not name:
|
||||
return ""
|
||||
value = request.headers.get(name) or request.headers.get(name.lower()) or request.headers.get(name.upper()) or ""
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
def _safe_username(value: str, fallback: str = "external-user") -> str:
|
||||
raw = str(value or "").strip()
|
||||
if "@" in raw:
|
||||
raw = raw.split("@", 1)[0]
|
||||
clean = "".join(ch for ch in raw if ch.isalnum() or ch in {".", "_", "-"}).strip("._-")
|
||||
return (clean or fallback)[:80]
|
||||
|
||||
|
||||
def _external_identity_from_headers() -> dict[str, str] | None:
|
||||
username = _clean_header_value(AUTH_PROXY_USER_HEADER)
|
||||
email = _clean_header_value(AUTH_PROXY_EMAIL_HEADER)
|
||||
display_name = _clean_header_value(AUTH_PROXY_NAME_HEADER)
|
||||
subject = _clean_header_value(AUTH_PROXY_SUBJECT_HEADER) if AUTH_PROXY_SUBJECT_HEADER else ""
|
||||
if not username and email:
|
||||
username = email.split("@", 1)[0]
|
||||
if not username:
|
||||
return None
|
||||
return {
|
||||
"provider": provider(),
|
||||
"username": _safe_username(username),
|
||||
"email": email[:254],
|
||||
"display_name": display_name[:160],
|
||||
"subject": (subject or username or email)[:254],
|
||||
}
|
||||
|
||||
|
||||
def _grant_default_external_permissions(conn, user_id: int, now: str) -> None:
|
||||
if AUTH_PROXY_DEFAULT_ACCESS == "none" or AUTH_PROXY_DEFAULT_ROLE == "admin":
|
||||
return
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO user_profile_permissions(user_id,profile_id,access_level,created_at,updated_at) VALUES(?,?,?,?,?)",
|
||||
(user_id, 0, AUTH_PROXY_DEFAULT_ACCESS, now, now),
|
||||
)
|
||||
|
||||
|
||||
def authenticate_external_user() -> dict[str, Any] | None:
|
||||
if not uses_external_provider():
|
||||
return None
|
||||
identity = _external_identity_from_headers()
|
||||
if not identity:
|
||||
return None
|
||||
now = utcnow()
|
||||
with connect() as conn:
|
||||
user = None
|
||||
if identity["subject"]:
|
||||
user = conn.execute(
|
||||
"SELECT * FROM users WHERE external_auth_provider=? AND external_subject=?",
|
||||
(identity["provider"], identity["subject"]),
|
||||
).fetchone()
|
||||
if not user:
|
||||
user = conn.execute("SELECT * FROM users WHERE username=?", (identity["username"],)).fetchone()
|
||||
if not user and identity["email"]:
|
||||
user = conn.execute("SELECT * FROM users WHERE lower(email)=lower(?)", (identity["email"],)).fetchone()
|
||||
if not user:
|
||||
if not AUTH_PROXY_AUTO_CREATE:
|
||||
return None
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO users(username,password_hash,email,display_name,external_auth_provider,external_subject,role,is_active,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
identity["username"],
|
||||
None,
|
||||
identity["email"] or None,
|
||||
identity["display_name"] or None,
|
||||
identity["provider"],
|
||||
identity["subject"] or identity["username"],
|
||||
AUTH_PROXY_DEFAULT_ROLE,
|
||||
1,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
user_id = int(cur.lastrowid)
|
||||
_grant_default_external_permissions(conn, user_id, now)
|
||||
user = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone()
|
||||
else:
|
||||
user_id = int(user["id"])
|
||||
conn.execute(
|
||||
"""UPDATE users
|
||||
SET email=COALESCE(NULLIF(?, ''), email),
|
||||
display_name=COALESCE(NULLIF(?, ''), display_name),
|
||||
external_auth_provider=?,
|
||||
external_subject=COALESCE(NULLIF(?, ''), external_subject),
|
||||
updated_at=?
|
||||
WHERE id=?""",
|
||||
(identity["email"], identity["display_name"], identity["provider"], identity["subject"], now, user_id),
|
||||
)
|
||||
user = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone()
|
||||
if not user or not int(user.get("is_active") or 0):
|
||||
return None
|
||||
g.external_user_id = int(user["id"])
|
||||
return _public_user(user)
|
||||
|
||||
|
||||
def logout_user() -> None:
|
||||
session.clear()
|
||||
|
||||
@@ -236,7 +362,7 @@ def list_users() -> list[dict[str, Any]]:
|
||||
require_admin()
|
||||
with connect() as conn:
|
||||
users = conn.execute(
|
||||
"SELECT id, username, role, is_active, created_at, updated_at FROM users ORDER BY username COLLATE NOCASE"
|
||||
"SELECT id, username, email, display_name, external_auth_provider, external_subject, role, is_active, created_at, updated_at FROM users ORDER BY username COLLATE NOCASE"
|
||||
).fetchall()
|
||||
perms = conn.execute(
|
||||
"SELECT user_id, profile_id, access_level FROM user_profile_permissions ORDER BY user_id, profile_id"
|
||||
@@ -271,13 +397,13 @@ def save_user(data: dict[str, Any], user_id: int | None = None) -> dict[str, Any
|
||||
if not row:
|
||||
raise ValueError("User does not exist")
|
||||
conn.execute(
|
||||
"UPDATE users SET username=?, role=?, is_active=?, updated_at=? WHERE id=?",
|
||||
(username, role, is_active, now, user_id),
|
||||
"UPDATE users SET username=?, email=?, display_name=?, role=?, is_active=?, updated_at=? WHERE id=?",
|
||||
(username, str(data.get("email") or "").strip() or None, str(data.get("display_name") or "").strip() or None, role, is_active, now, user_id),
|
||||
)
|
||||
else:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO users(username,password_hash,role,is_active,created_at,updated_at) VALUES(?,?,?,?,?,?)",
|
||||
(username, password_hash(str(data.get("password") or username)), role, is_active, now, now),
|
||||
"INSERT INTO users(username,password_hash,email,display_name,role,is_active,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)",
|
||||
(username, password_hash(str(data.get("password") or username)), str(data.get("email") or "").strip() or None, str(data.get("display_name") or "").strip() or None, role, is_active, now, now),
|
||||
)
|
||||
user_id = int(cur.lastrowid)
|
||||
if data.get("password"):
|
||||
@@ -293,7 +419,7 @@ def save_user(data: dict[str, Any], user_id: int | None = None) -> dict[str, Any
|
||||
)
|
||||
else:
|
||||
conn.execute("DELETE FROM user_profile_permissions WHERE user_id=?", (user_id,))
|
||||
return conn.execute("SELECT id, username, role, is_active, created_at, updated_at FROM users WHERE id=?", (user_id,)).fetchone()
|
||||
return conn.execute("SELECT id, username, email, display_name, external_auth_provider, external_subject, role, is_active, created_at, updated_at FROM users WHERE id=?", (user_id,)).fetchone()
|
||||
|
||||
|
||||
def delete_user(user_id: int) -> None:
|
||||
@@ -323,6 +449,10 @@ def _public_user(row: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
return {
|
||||
"id": int(row["id"]),
|
||||
"username": row.get("username"),
|
||||
"email": row.get("email"),
|
||||
"display_name": row.get("display_name"),
|
||||
"external_auth_provider": row.get("external_auth_provider"),
|
||||
"external_subject": row.get("external_subject"),
|
||||
"role": row.get("role") or "user",
|
||||
"is_active": int(row.get("is_active") or 0),
|
||||
"created_at": row.get("created_at"),
|
||||
@@ -445,6 +575,8 @@ def install_guards(app) -> None:
|
||||
if token_user:
|
||||
g.api_user_id = int(token_user["id"])
|
||||
g.api_token_authenticated = True
|
||||
if not getattr(g, "api_user_id", None):
|
||||
authenticate_external_user()
|
||||
endpoint = request.endpoint or ""
|
||||
if endpoint in PUBLIC_ENDPOINTS or endpoint.startswith("static"):
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user