"""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.identity import remember 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 DETAIL_FIELDS, DetailsForm, 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"]}) # -- profile details (FR-9) ------------------------------------------------------------------- NOTICE_FLAG = "profile_notice" # "saved" or "unchanged", shown once after the redirect def _details_page(request, form, details, *, status=200, notice=None): context = { "form": form, "details": details, "notice": notice, "fields": [ {"field": form[name], "label": label, "max_length": max_length} for name, (label, max_length, _required) in DETAIL_FIELDS.items() ], } return render(request, "profile/details.html", context, status=status) @login_required @require_http_methods(["GET", "POST"]) def profile(request): user = request.portal_user.user # the session user only: nothing is read from the request if request.method == "GET": try: details = services.load_details(user) except DirectoryUnavailable: return _unavailable(request) notice = request.session.pop(NOTICE_FLAG, None) return _details_page(request, DetailsForm(initial=details.values), details, notice=notice) form = DetailsForm(request.POST) form.is_valid() # trims; the checks need the directory value and run in the service try: result = services.update_details(user, form.cleaned_data, ip=request.client_ip) except services.DetailsInvalid as exc: form.apply_errors(exc.errors) return _details_page(request, form, exc.details, status=400) except services.DetailsNotAccepted as exc: form.add_error(None, _("A value was not accepted. Please check your entries.")) return _details_page(request, form, exc.details, status=400) except DirectoryUnavailable: return _unavailable(request) except services.OutcomeUnknown: return render(request, "profile/details_unknown.html", status=500) if result.changed: remember(request, result.account) # this session shows the new name at once request.session[NOTICE_FLAG] = "saved" if result.changed else "unchanged" return redirect("profile")