stages/password: create default password change flow
This commit is contained in:
parent
d6a8d8292d
commit
21ba969072
|
@ -8,3 +8,4 @@ class PassbookStagePasswordConfig(AppConfig):
|
||||||
name = "passbook.stages.password"
|
name = "passbook.stages.password"
|
||||||
label = "passbook_stages_password"
|
label = "passbook_stages_password"
|
||||||
verbose_name = "passbook Stages.Password"
|
verbose_name = "passbook Stages.Password"
|
||||||
|
mountpoint = "-/user/stage/password/"
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django import forms
|
||||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from passbook.flows.models import Flow, FlowDesignation
|
||||||
from passbook.stages.password.models import PasswordStage
|
from passbook.stages.password.models import PasswordStage
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,14 +41,19 @@ class PasswordForm(forms.Form):
|
||||||
class PasswordStageForm(forms.ModelForm):
|
class PasswordStageForm(forms.ModelForm):
|
||||||
"""Form to create/edit Password Stages"""
|
"""Form to create/edit Password Stages"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["change_flow"].queryset = Flow.objects.filter(
|
||||||
|
designation=FlowDesignation.STAGE_SETUP
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = PasswordStage
|
model = PasswordStage
|
||||||
fields = ["name", "backends"]
|
fields = ["name", "backends", "change_flow"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
"backends": FilteredSelectMultiple(
|
"backends": FilteredSelectMultiple(
|
||||||
_("backends"), False, choices=get_authentication_backends()
|
_("backends"), False, choices=get_authentication_backends()
|
||||||
),
|
),
|
||||||
"password_policies": FilteredSelectMultiple(_("password policies"), False),
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Generated by Django 3.0.7 on 2020-06-29 08:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
from passbook.flows.models import FlowDesignation
|
||||||
|
from passbook.stages.prompt.models import FieldTypes
|
||||||
|
|
||||||
|
PROMPT_POLICY_EXPRESSION = """# Check that both passwords are equal.
|
||||||
|
return request.context['password'] == request.context['password_repeat']"""
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_password_change(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
Flow = apps.get_model("passbook_flows", "Flow")
|
||||||
|
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
|
||||||
|
|
||||||
|
PolicyBinding = apps.get_model("passbook_policies", "PolicyBinding")
|
||||||
|
|
||||||
|
ExpressionPolicy = apps.get_model(
|
||||||
|
"passbook_policies_expression", "ExpressionPolicy"
|
||||||
|
)
|
||||||
|
|
||||||
|
PromptStage = apps.get_model("passbook_stages_prompt", "PromptStage")
|
||||||
|
Prompt = apps.get_model("passbook_stages_prompt", "Prompt")
|
||||||
|
|
||||||
|
UserWriteStage = apps.get_model("passbook_stages_user_write", "UserWriteStage")
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||||
|
slug="default-password-change",
|
||||||
|
designation=FlowDesignation.STAGE_SETUP,
|
||||||
|
defaults={"name": "Change Password"},
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-password-change-prompt",
|
||||||
|
)
|
||||||
|
password_prompt, _ = Prompt.objects.using(db_alias).update_or_create(
|
||||||
|
field_key="password",
|
||||||
|
defaults={
|
||||||
|
"label": "Password",
|
||||||
|
"type": FieldTypes.PASSWORD,
|
||||||
|
"required": True,
|
||||||
|
"placeholder": "Password",
|
||||||
|
"order": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
password_rep_prompt, _ = Prompt.objects.using(db_alias).update_or_create(
|
||||||
|
field_key="password_repeat",
|
||||||
|
defaults={
|
||||||
|
"label": "Password (repeat)",
|
||||||
|
"type": FieldTypes.PASSWORD,
|
||||||
|
"required": True,
|
||||||
|
"placeholder": "Password (repeat)",
|
||||||
|
"order": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
prompt_stage.fields.add(password_prompt)
|
||||||
|
prompt_stage.fields.add(password_rep_prompt)
|
||||||
|
|
||||||
|
# Policy to only trigger prompt when no username is given
|
||||||
|
prompt_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-password-change-password-equal",
|
||||||
|
defaults={"expression": PROMPT_POLICY_EXPRESSION},
|
||||||
|
)
|
||||||
|
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||||
|
policy=prompt_policy, target=prompt_stage, defaults={"order": 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
user_write, _ = UserWriteStage.objects.using(db_alias).update_or_create(
|
||||||
|
name="default-password-change-write"
|
||||||
|
)
|
||||||
|
|
||||||
|
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||||
|
flow=flow, stage=prompt_stage, defaults={"order": 0}
|
||||||
|
)
|
||||||
|
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||||
|
flow=flow, stage=user_write, defaults={"order": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_flows", "0006_auto_20200629_0857"),
|
||||||
|
("passbook_policies_expression", "0001_initial"),
|
||||||
|
("passbook_policies", "0001_initial"),
|
||||||
|
("passbook_stages_password", "0001_initial"),
|
||||||
|
("passbook_stages_prompt", "0004_auto_20200618_1735"),
|
||||||
|
("passbook_stages_user_write", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="passwordstage",
|
||||||
|
name="change_flow",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Flow used by an authenticated user to change their password. If empty, user will be unable to change their password.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="passbook_flows.Flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(create_default_password_change),
|
||||||
|
]
|
|
@ -1,9 +1,15 @@
|
||||||
"""password stage models"""
|
"""password stage models"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.utils.http import urlencode
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from passbook.flows.models import Stage
|
from passbook.core.types import UIUserSettings
|
||||||
|
from passbook.flows.models import Flow, Stage
|
||||||
|
from passbook.flows.views import NEXT_ARG_NAME
|
||||||
|
|
||||||
|
|
||||||
class PasswordStage(Stage):
|
class PasswordStage(Stage):
|
||||||
|
@ -14,9 +20,32 @@ class PasswordStage(Stage):
|
||||||
help_text=_("Selection of backends to test the password against."),
|
help_text=_("Selection of backends to test the password against."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
change_flow = models.ForeignKey(
|
||||||
|
Flow,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_(
|
||||||
|
(
|
||||||
|
"Flow used by an authenticated user to change their password. "
|
||||||
|
"If empty, user will be unable to change their password."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
type = "passbook.stages.password.stage.PasswordStage"
|
type = "passbook.stages.password.stage.PasswordStage"
|
||||||
form = "passbook.stages.password.forms.PasswordStageForm"
|
form = "passbook.stages.password.forms.PasswordStageForm"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ui_user_settings(self) -> Optional[UIUserSettings]:
|
||||||
|
if not self.change_flow:
|
||||||
|
return None
|
||||||
|
base_url = reverse(
|
||||||
|
"passbook_stages_password:change", kwargs={"stage_uuid": self.pk}
|
||||||
|
)
|
||||||
|
args = urlencode({NEXT_ARG_NAME: reverse("passbook_core:user-settings")})
|
||||||
|
return UIUserSettings(name=self.name, url=f"{base_url}?{args}")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Password Stage {self.name}"
|
return f"Password Stage {self.name}"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
"""password stage URLs"""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from passbook.stages.password.views import ChangeFlowInitView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("<uuid:stage_uuid>/change/", ChangeFlowInitView.as_view(), name="change")
|
||||||
|
]
|
|
@ -0,0 +1,32 @@
|
||||||
|
"""password stage views"""
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import Http404, HttpRequest, HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||||
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
|
from passbook.lib.utils.urls import redirect_with_qs
|
||||||
|
from passbook.stages.password.models import PasswordStage
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeFlowInitView(LoginRequiredMixin, View):
|
||||||
|
"""Initiate planner for selected change flow and redirect to flow executor,
|
||||||
|
or raise Http404 if no change_flow has been set."""
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, stage_uuid: str) -> HttpResponse:
|
||||||
|
"""Initiate planner for selected change flow and redirect to flow executor,
|
||||||
|
or raise Http404 if no change_flow has been set."""
|
||||||
|
stage: PasswordStage = get_object_or_404(PasswordStage, pk=stage_uuid)
|
||||||
|
if not stage.change_flow:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
plan = FlowPlanner(stage.change_flow).plan(
|
||||||
|
request, {PLAN_CONTEXT_PENDING_USER: request.user,}
|
||||||
|
)
|
||||||
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
return redirect_with_qs(
|
||||||
|
"passbook_flows:flow-executor-shell",
|
||||||
|
self.request.GET,
|
||||||
|
flow_slug=stage.change_flow.slug,
|
||||||
|
)
|
|
@ -1,5 +1,6 @@
|
||||||
"""Write stage logic"""
|
"""Write stage logic"""
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.backends import ModelBackend
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
@ -48,6 +49,10 @@ class UserWriteStageView(StageView):
|
||||||
else:
|
else:
|
||||||
user.attributes[key] = value
|
user.attributes[key] = value
|
||||||
user.save()
|
user.save()
|
||||||
|
# Check if the password has been updated, and update the session auth hash
|
||||||
|
if any(["password" in x for x in data.keys()]):
|
||||||
|
update_session_auth_hash(self.request, user)
|
||||||
|
LOGGER.debug("Updated session hash", user=user)
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Updated existing user", user=user, flow_slug=self.executor.flow.slug,
|
"Updated existing user", user=user, flow_slug=self.executor.flow.slug,
|
||||||
)
|
)
|
||||||
|
|
Reference in New Issue