from django.conf import settings from django.contrib.auth import logout from django.contrib.auth import views as auth_views from django.contrib.auth.decorators import login_required from django.core.exceptions import ValidationError from django.db import IntegrityError from django.shortcuts import redirect, render from urllib.parse import urlparse from apps.keys.certificates import issue_certificate_for_key from apps.keys.models import SSHKey from .forms import ErasureRequestForm, SSHKeyForm from .models import ErasureRequest, ExternalIdentity def _oidc_provider_label() -> str: endpoint = ( getattr(settings, "OIDC_OP_AUTHORIZATION_ENDPOINT", "") or getattr(settings, "OIDC_OP_DISCOVERY_ENDPOINT", "") or getattr(settings, "OIDC_OP_USER_ENDPOINT", "") or "" ) if not endpoint: return "Identity Provider" parsed = urlparse(endpoint) host = (parsed.hostname or "").strip().lower() if not host: return "Identity Provider" parts = [part for part in host.split(".") if part and part != "www"] if len(parts) >= 2: return parts[-2].replace("-", " ").title() return host.replace("-", " ").title() @login_required(login_url="/accounts/login/") def profile(request): erasure_request = ( ErasureRequest.objects.filter(user=request.user).order_by("-requested_at").first() ) can_add_key = request.user.has_perm("keys.add_sshkey") if request.method == "POST": form_type = request.POST.get("form_type") if form_type == "ssh_key": erasure_form = ErasureRequestForm() key_form = SSHKeyForm(request.POST) if key_form.is_valid(): if not can_add_key: key_form.add_error(None, "You do not have permission to add SSH keys.") else: name = key_form.cleaned_data["name"].strip() public_key = key_form.cleaned_data["public_key"].strip() key = SSHKey(user=request.user, name=name) try: key.set_public_key(public_key) key.save() issue_certificate_for_key(key, created_by=request.user) return redirect("accounts:profile") except ValidationError as exc: key_form.add_error("public_key", str(exc)) except IntegrityError: key_form.add_error("public_key", "Key already exists.") except Exception: key_form.add_error(None, "Certificate issuance failed.") else: key_form = SSHKeyForm() erasure_form = ErasureRequestForm(request.POST) if erasure_form.is_valid(): if erasure_request and erasure_request.status == ErasureRequest.Status.PENDING: erasure_form.add_error(None, "You already have a pending erasure request.") else: ErasureRequest.objects.create( user=request.user, reason=erasure_form.cleaned_data["reason"].strip(), ) return redirect("accounts:profile") else: erasure_form = ErasureRequestForm() key_form = SSHKeyForm() ssh_keys = SSHKey.objects.filter(user=request.user).order_by("-created_at") external_identities = ExternalIdentity.objects.filter(user=request.user).order_by("-last_login_at", "provider_id") oidc_identity = external_identities.filter(provider_type=ExternalIdentity.ProviderType.OIDC).first() social_identity_map = { identity.provider_id.lower(): identity for identity in external_identities.filter(provider_type=ExternalIdentity.ProviderType.SOCIAL) } configured_social_map = { provider.get("id", "").strip().lower(): provider for provider in getattr(settings, "KEYWARDEN_SOCIAL_LOGIN_PROVIDERS", []) if provider.get("id") } social_provider_defaults = [ ("github", "GitHub"), ("google", "Google"), ("apple", "Apple"), ("gitlab", "GitLab"), ("microsoft", "Microsoft Entra ID"), ] social_provider_cards = [] seen_provider_ids = set() for provider_id, provider_name in social_provider_defaults: provider_config = configured_social_map.get(provider_id, {}) identity = social_identity_map.get(provider_id) social_provider_cards.append( { "id": provider_id, "name": provider_name, "configured": bool(provider_config.get("login_url")), "login_url": provider_config.get("login_url", ""), "linked": bool(identity), "identity": identity, } ) seen_provider_ids.add(provider_id) for provider_id, provider_config in configured_social_map.items(): if provider_id in seen_provider_ids: continue identity = social_identity_map.get(provider_id) social_provider_cards.append( { "id": provider_id, "name": provider_config.get("name", provider_id.title()), "configured": bool(provider_config.get("login_url")), "login_url": provider_config.get("login_url", ""), "linked": bool(identity), "identity": identity, } ) context = { "user": request.user, "auth_mode": getattr(settings, "KEYWARDEN_AUTH_MODE", "hybrid"), "oidc_enabled": bool(getattr(settings, "KEYWARDEN_OIDC_ENABLED", False)), "oidc_identity": oidc_identity, "oidc_provider_id": getattr(settings, "KEYWARDEN_OIDC_PROVIDER_ID", "oidc"), "oidc_issuer": getattr(settings, "KEYWARDEN_OIDC_ISSUER", ""), "social_provider_cards": social_provider_cards, "erasure_request": erasure_request, "erasure_form": erasure_form, "key_form": key_form, "ssh_keys": ssh_keys, "can_add_key": can_add_key, } return render(request, "accounts/profile.html", context) def login_view(request): auth_mode = getattr(settings, "KEYWARDEN_AUTH_MODE", "hybrid") if auth_mode == "oidc": return redirect("/oidc/authenticate/") has_sso_options = bool( getattr(settings, "KEYWARDEN_OIDC_ENABLED", False) or getattr(settings, "KEYWARDEN_SOCIAL_LOGIN_PROVIDERS", []) ) show_native_login = ( request.method == "POST" or request.GET.get("native") == "1" or (auth_mode == "native" and not has_sso_options) ) # native or hybrid -> render Django's built-in login view return auth_views.LoginView.as_view( template_name="accounts/login.html", extra_context={ "social_providers": getattr(settings, "KEYWARDEN_SOCIAL_LOGIN_PROVIDERS", []), "oidc_enabled": bool(getattr(settings, "KEYWARDEN_OIDC_ENABLED", False)), "oidc_login_url": "/oidc/authenticate/", "oidc_provider_label": _oidc_provider_label(), "show_native_login": show_native_login, "can_use_native_login": auth_mode in {"native", "hybrid"}, "show_sso_choice": has_sso_options and not show_native_login, }, )(request) def logout_view(request): logout(request) return redirect(getattr(settings, "LOGOUT_REDIRECT_URL", "/"))