"""The password page (FR-5..FR-7). No business logic here: see services.py (AD-17).""" from __future__ import annotations import logging from django.shortcuts import redirect, render from django.utils.translation import gettext as _ from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.http import require_http_methods from portal.accounts.decorators import login_required from portal.accounts.throttle import Throttled from portal.adapters.directory import DirectoryUnavailable from portal.adapters.registry import get_directory from . import services from .forms import PasswordChangeForm logger = logging.getLogger(__name__) DONE_FLAG = "password_changed" # Rule keys from services.local_hints, in plain words. HINT_MESSAGES = { "too_short": lambda: _("The password is too short."), "too_few_groups": lambda: _( "The password needs characters from more groups: upper case, lower case, digits, special." ), } def _page(request, form, *, policy=None, status=200, throttled=False): rules = services.describe_policy(policy) if policy is not None else [] partly_known = policy is not None and policy.fine_grained and not policy.fine_grained_readable context = { "form": form, "rules": rules, "partly_known": partly_known, "throttled": throttled, } return render(request, "profile/password.html", context, status=status) def _unavailable(request): return render(request, "accounts/unavailable.html", status=503) @login_required @sensitive_post_parameters("current_password", "new_password", "confirm_password") @require_http_methods(["GET", "POST"]) def password(request): user = request.portal_user.user if request.method == "GET": try: policy = get_directory().password_policy(user.sam) except DirectoryUnavailable: return _unavailable(request) return _page(request, PasswordChangeForm(), policy=policy) form = PasswordChangeForm(request.POST) if not form.is_valid(): return _page(request, form, policy=_policy_or_none(user), status=400) try: result = services.change_password( user, form.cleaned_data["current_password"], form.cleaned_data["new_password"], ip=request.client_ip, session_key=request.session.session_key, ) except Throttled: return _page(request, PasswordChangeForm(), status=429, throttled=True) except services.WrongCurrentPassword as exc: form.add_error("current_password", _("The current password is not correct.")) return _page(request, form, policy=exc.policy, status=400) except services.PasswordNotAccepted as exc: messages = [HINT_MESSAGES[key]() for key in exc.reasons] or [ _( "The new password does not meet the rules above, " "or it was used or changed too recently." ) ] for message in messages: form.add_error("new_password", message) return _page(request, form, policy=exc.policy, status=400) except DirectoryUnavailable: return _unavailable(request) except services.OutcomeUnknown: return render(request, "profile/password_unknown.html", status=500) request.session[DONE_FLAG] = {"mail": result.mail_queued} return redirect("password-done") def _policy_or_none(user): try: return get_directory().password_policy(user.sam) except DirectoryUnavailable: return None @login_required def password_done(request): flag = request.session.pop(DONE_FLAG, None) if flag is None: # opened directly: nothing to report return redirect("password") return render(request, "profile/password_done.html", {"mail": flag["mail"]})