diff --git a/backend/atlas_portal/routes/access_requests.py b/backend/atlas_portal/routes/access_requests.py index 7184746..b688162 100644 --- a/backend/atlas_portal/routes/access_requests.py +++ b/backend/atlas_portal/routes/access_requests.py @@ -9,13 +9,13 @@ import string from typing import Any from urllib.parse import quote -from flask import jsonify, request, g, redirect +from flask import jsonify, request, redirect import psycopg from .. import ariadne_client from ..db import connect, configured -from ..keycloak import admin_client, oidc_client, require_auth +from ..keycloak import admin_client, oidc_client from ..mailer import MailerError, access_request_verification_body, send_text_email from ..rate_limit import rate_limit_allow from ..provisioning import provision_access_request, provision_tasks_complete @@ -152,7 +152,6 @@ ONBOARDING_STEPS: tuple[str, ...] = ( "vaultwarden_mobile_app", "keycloak_password_rotated", "element_recovery_key", - "element_recovery_key_stored", "element_mobile_app", "mail_client_setup", "nextcloud_web_access", @@ -181,7 +180,6 @@ ONBOARDING_REQUIRED_STEPS: tuple[str, ...] = ( "vaultwarden_mobile_app", "keycloak_password_rotated", "element_recovery_key", - "element_recovery_key_stored", "mail_client_setup", "nextcloud_web_access", "nextcloud_mail_integration", @@ -204,7 +202,6 @@ ONBOARDING_STEP_PREREQUISITES: dict[str, set[str]] = { "vaultwarden_mobile_app": {"vaultwarden_master_password"}, "keycloak_password_rotated": {"vaultwarden_master_password"}, "element_recovery_key": {"keycloak_password_rotated"}, - "element_recovery_key_stored": {"element_recovery_key"}, "element_mobile_app": {"element_recovery_key"}, "mail_client_setup": {"vaultwarden_master_password"}, "nextcloud_web_access": {"vaultwarden_master_password"}, @@ -212,15 +209,13 @@ ONBOARDING_STEP_PREREQUISITES: dict[str, set[str]] = { "nextcloud_desktop_app": {"nextcloud_web_access"}, "nextcloud_mobile_app": {"nextcloud_web_access"}, "budget_encryption_ack": {"nextcloud_mail_integration"}, - "firefly_password_rotated": {"element_recovery_key_stored"}, + "firefly_password_rotated": {"element_recovery_key"}, "wger_password_rotated": {"firefly_password_rotated"}, "jellyfin_web_access": {"vaultwarden_master_password"}, "jellyfin_mobile_app": {"jellyfin_web_access"}, "jellyfin_tv_setup": {"jellyfin_web_access"}, } -_ELEMENT_RECOVERY_ARTIFACT = "element_recovery_key_sha256" -_SHA256_HEX_RE = re.compile(r"^[0-9a-f]{64}$") _VAULTWARDEN_READY_STATUSES = {"already_present", "active", "ready"} @@ -959,14 +954,9 @@ def register(app) -> None: mark_done = completed if mark_done: - if step == "element_recovery_key": - return ( - jsonify({"error": "step requires verification"}), - 400, - ) prerequisites = ONBOARDING_STEP_PREREQUISITES.get(step, set()) if prerequisites: - current_completed = _completed_onboarding_steps(conn, code, username) + current_completed = _completed_onboarding_steps(conn, code, row.get("username") or "") missing = sorted(prerequisites - current_completed) if missing: return jsonify({"error": "step is blocked", "blocked_by": missing}), 409 @@ -989,15 +979,11 @@ def register(app) -> None: "DELETE FROM access_request_onboarding_steps WHERE request_code = %s AND step = %s", (code, step), ) - if step == "element_recovery_key": - conn.execute( - "DELETE FROM access_request_onboarding_artifacts WHERE request_code = %s AND artifact = %s", - (code, _ELEMENT_RECOVERY_ARTIFACT), - ) # Re-evaluate completion to update request status to ready if applicable. - status = _advance_status(conn, code, username, status) - onboarding_payload = _onboarding_payload(conn, code, username) + request_username = row.get("username") or "" + status = _advance_status(conn, code, request_username, status) + onboarding_payload = _onboarding_payload(conn, code, request_username) except Exception: return jsonify({"error": "failed to update onboarding"}), 502 @@ -1009,83 +995,7 @@ def register(app) -> None: } ) - @app.route("/api/access/request/onboarding/element-recovery", methods=["POST"]) - @require_auth - def request_access_onboarding_element_recovery() -> Any: - if not configured(): - return jsonify({"error": "server not configured"}), 503 - - payload = request.get_json(silent=True) or {} - code = (payload.get("request_code") or payload.get("code") or "").strip() - sha256_hex = (payload.get("sha256") or payload.get("sha256_hex") or "").strip().lower() - - if not code: - return jsonify({"error": "request_code is required"}), 400 - if not sha256_hex: - return jsonify({"error": "sha256 is required"}), 400 - if not _SHA256_HEX_RE.fullmatch(sha256_hex): - return jsonify({"error": "invalid sha256"}), 400 - - username = getattr(g, "keycloak_username", "") or "" - if not username: - return jsonify({"error": "invalid token"}), 401 - - try: - with connect() as conn: - row = conn.execute( - "SELECT username, status FROM access_requests WHERE request_code = %s", - (code,), - ).fetchone() - if not row: - return jsonify({"error": "not found"}), 404 - if (row.get("username") or "") != username: - return jsonify({"error": "forbidden"}), 403 - - status = _normalize_status(row.get("status") or "") - if status not in {"awaiting_onboarding", "ready"}: - return jsonify({"error": "onboarding not available"}), 409 - - prerequisites = ONBOARDING_STEP_PREREQUISITES.get("element_recovery_key", set()) - if prerequisites: - current_completed = _completed_onboarding_steps(conn, code, username) - missing = sorted(prerequisites - current_completed) - if missing: - return jsonify({"error": "step is blocked", "blocked_by": missing}), 409 - - conn.execute( - """ - INSERT INTO access_request_onboarding_artifacts (request_code, artifact, value_hash) - VALUES (%s, %s, %s) - ON CONFLICT (request_code, artifact) DO UPDATE - SET value_hash = EXCLUDED.value_hash, - created_at = NOW() - """, - (code, _ELEMENT_RECOVERY_ARTIFACT, sha256_hex), - ) - conn.execute( - """ - INSERT INTO access_request_onboarding_steps (request_code, step) - VALUES (%s, %s) - ON CONFLICT (request_code, step) DO NOTHING - """, - (code, "element_recovery_key"), - ) - - status = _advance_status(conn, code, username, status) - onboarding_payload = _onboarding_payload(conn, code, username) - except Exception: - return jsonify({"error": "failed to verify element recovery key"}), 502 - - return jsonify( - { - "ok": True, - "status": status, - "onboarding": onboarding_payload, - } - ) - @app.route("/api/access/request/onboarding/keycloak-password-rotate", methods=["POST"]) - @require_auth def request_access_onboarding_keycloak_password_rotate() -> Any: if not configured(): return jsonify({"error": "server not configured"}), 503 @@ -1095,9 +1005,20 @@ def register(app) -> None: if not code: return jsonify({"error": "request_code is required"}), 400 - username = getattr(g, "keycloak_username", "") or "" - if not username: - return jsonify({"error": "invalid token"}), 401 + token_username = "" + bearer = request.headers.get("Authorization", "") + if bearer: + parts = bearer.split(None, 1) + if len(parts) != 2 or parts[0].lower() != "bearer": + return jsonify({"error": "invalid token"}), 401 + token = parts[1].strip() + if not token: + return jsonify({"error": "invalid token"}), 401 + try: + claims = oidc_client().verify(token) + except Exception: + return jsonify({"error": "invalid token"}), 401 + token_username = claims.get("preferred_username") or "" if not admin_client().ready(): return jsonify({"error": "keycloak admin unavailable"}), 503 @@ -1110,7 +1031,8 @@ def register(app) -> None: ).fetchone() if not row: return jsonify({"error": "not found"}), 404 - if (row.get("username") or "") != username: + request_username = row.get("username") or "" + if token_username and request_username != token_username: return jsonify({"error": "forbidden"}), 403 status = _normalize_status(row.get("status") or "") @@ -1119,12 +1041,12 @@ def register(app) -> None: prerequisites = ONBOARDING_STEP_PREREQUISITES.get("keycloak_password_rotated", set()) if prerequisites: - current_completed = _completed_onboarding_steps(conn, code, username) + current_completed = _completed_onboarding_steps(conn, code, request_username) missing = sorted(prerequisites - current_completed) if missing: return jsonify({"error": "step is blocked", "blocked_by": missing}), 409 - user = admin_client().find_user(username) or {} + user = admin_client().find_user(request_username) or {} user_id = user.get("id") if isinstance(user, dict) else None if not isinstance(user_id, str) or not user_id: return jsonify({"error": "keycloak user not found"}), 409 @@ -1147,7 +1069,7 @@ def register(app) -> None: (code, _KEYCLOAK_PASSWORD_ROTATION_REQUESTED_ARTIFACT), ) - onboarding_payload = _onboarding_payload(conn, code, username) + onboarding_payload = _onboarding_payload(conn, code, request_username) except Exception: return jsonify({"error": "failed to request password rotation"}), 502 diff --git a/frontend/src/views/OnboardingView.vue b/frontend/src/views/OnboardingView.vue index 8dc6b02..fc002fc 100644 --- a/frontend/src/views/OnboardingView.vue +++ b/frontend/src/views/OnboardingView.vue @@ -161,7 +161,7 @@ {{ step.title }} @@ -233,56 +233,7 @@

Guide coming soon.

-
- - - -
- -
- +