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.
-
-
-
-
-
-
-
-
-
- Open Element
-
+