stages/prompt: add prompt stage: dynamically created forms based on database
This commit is contained in:
parent
9def45c8d7
commit
4315d1a03c
|
@ -37,6 +37,7 @@ from passbook.stages.identification.api import IdentificationStageViewSet
|
||||||
from passbook.stages.login.api import LoginStageViewSet
|
from passbook.stages.login.api import LoginStageViewSet
|
||||||
from passbook.stages.otp.api import OTPStageViewSet
|
from passbook.stages.otp.api import OTPStageViewSet
|
||||||
from passbook.stages.password.api import PasswordStageViewSet
|
from passbook.stages.password.api import PasswordStageViewSet
|
||||||
|
from passbook.stages.prompt.api import PromptStageViewSet, PromptViewSet
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
|
@ -83,6 +84,8 @@ router.register("stages/otp", OTPStageViewSet)
|
||||||
router.register("stages/password", PasswordStageViewSet)
|
router.register("stages/password", PasswordStageViewSet)
|
||||||
router.register("stages/identification", IdentificationStageViewSet)
|
router.register("stages/identification", IdentificationStageViewSet)
|
||||||
router.register("stages/login", LoginStageViewSet)
|
router.register("stages/login", LoginStageViewSet)
|
||||||
|
router.register("stages/prompt", PromptStageViewSet)
|
||||||
|
router.register("stages/prompt/prompts", PromptViewSet)
|
||||||
|
|
||||||
router.register("flows", FlowViewSet)
|
router.register("flows", FlowViewSet)
|
||||||
router.register("flows/bindings", FlowStageBindingViewSet)
|
router.register("flows/bindings", FlowStageBindingViewSet)
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
"""passbook core authentication forms"""
|
|
||||||
from django import forms
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from structlog import get_logger
|
|
||||||
|
|
||||||
from passbook.core.models import User
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class SignUpForm(forms.Form):
|
|
||||||
"""SignUp Form"""
|
|
||||||
|
|
||||||
title = _("Sign Up")
|
|
||||||
name = forms.CharField(
|
|
||||||
label=_("Name"), widget=forms.TextInput(attrs={"placeholder": _("Name")})
|
|
||||||
)
|
|
||||||
username = forms.CharField(
|
|
||||||
label=_("Username"),
|
|
||||||
widget=forms.TextInput(attrs={"placeholder": _("Username")}),
|
|
||||||
)
|
|
||||||
email = forms.EmailField(
|
|
||||||
label=_("E-Mail"), widget=forms.TextInput(attrs={"placeholder": _("E-Mail")})
|
|
||||||
)
|
|
||||||
password = forms.CharField(
|
|
||||||
label=_("Password"),
|
|
||||||
widget=forms.PasswordInput(attrs={"placeholder": _("Password")}),
|
|
||||||
)
|
|
||||||
password_repeat = forms.CharField(
|
|
||||||
label=_("Repeat Password"),
|
|
||||||
widget=forms.PasswordInput(attrs={"placeholder": _("Repeat Password")}),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
# All fields which have initial data supplied are set to read only
|
|
||||||
if "initial" in kwargs:
|
|
||||||
for field in kwargs.get("initial").keys():
|
|
||||||
self.fields[field].widget.attrs["readonly"] = "readonly"
|
|
||||||
|
|
||||||
def clean_username(self):
|
|
||||||
"""Check if username is used already"""
|
|
||||||
username = self.cleaned_data.get("username")
|
|
||||||
if User.objects.filter(username=username).exists():
|
|
||||||
LOGGER.warning("username already exists", username=username)
|
|
||||||
raise ValidationError(_("Username already exists"))
|
|
||||||
return username
|
|
||||||
|
|
||||||
def clean_email(self):
|
|
||||||
"""Check if email is already used in django or other auth sources"""
|
|
||||||
email = self.cleaned_data.get("email")
|
|
||||||
# Check if user exists already, error early
|
|
||||||
if User.objects.filter(email=email).exists():
|
|
||||||
LOGGER.debug("email already exists", email=email)
|
|
||||||
raise ValidationError(_("Email already exists"))
|
|
||||||
return email
|
|
||||||
|
|
||||||
def clean_password_repeat(self):
|
|
||||||
"""Check if Password adheres to filter and if passwords matche"""
|
|
||||||
password = self.cleaned_data.get("password")
|
|
||||||
password_repeat = self.cleaned_data.get("password_repeat")
|
|
||||||
if password != password_repeat:
|
|
||||||
raise ValidationError(_("Passwords don't match"))
|
|
||||||
return self.cleaned_data.get("password_repeat")
|
|
|
@ -5,7 +5,6 @@ from random import SystemRandom
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from passbook.core.forms.authentication import SignUpForm
|
|
||||||
from passbook.core.models import User
|
from passbook.core.models import User
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,12 +32,6 @@ class TestAuthenticationViews(TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_sign_up_view(self):
|
|
||||||
"""Test account.sign_up view (Anonymous)"""
|
|
||||||
self.client.logout()
|
|
||||||
response = self.client.get(reverse("passbook_core:auth-sign-up"))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_logout_view(self):
|
def test_logout_view(self):
|
||||||
"""Test account.logout view"""
|
"""Test account.logout view"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
@ -50,81 +43,3 @@ class TestAuthenticationViews(TestCase):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.get(reverse("passbook_core:auth-logout"))
|
response = self.client.get(reverse("passbook_core:auth-logout"))
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
def test_sign_up_view_post(self):
|
|
||||||
"""Test account.sign_up view POST (Anonymous)"""
|
|
||||||
form = SignUpForm(self.sign_up_data)
|
|
||||||
self.assertTrue(form.is_valid())
|
|
||||||
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("passbook_core:auth-sign-up"), data=form.cleaned_data
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
|
|
||||||
# def test_reset_password_init_view(self):
|
|
||||||
# """Test account.reset_password_init view POST (Anonymous)"""
|
|
||||||
# form = SignUpForm(self.sign_up_data)
|
|
||||||
# self.assertTrue(form.is_valid())
|
|
||||||
|
|
||||||
# res = test_request(accounts.SignUpView.as_view(),
|
|
||||||
# method='POST',
|
|
||||||
# req_kwargs=form.cleaned_data)
|
|
||||||
# self.assertEqual(res.status_code, 302)
|
|
||||||
|
|
||||||
# res = test_request(accounts.PasswordResetInitView.as_view())
|
|
||||||
# self.assertEqual(res.status_code, 200)
|
|
||||||
|
|
||||||
# def test_resend_confirmation(self):
|
|
||||||
# """Test AccountController.resend_confirmation"""
|
|
||||||
# form = SignUpForm(self.sign_up_data)
|
|
||||||
# self.assertTrue(form.is_valid())
|
|
||||||
|
|
||||||
# res = test_request(accounts.SignUpView.as_view(),
|
|
||||||
# method='POST',
|
|
||||||
# req_kwargs=form.cleaned_data)
|
|
||||||
# self.assertEqual(res.status_code, 302)
|
|
||||||
# user = User.objects.get(email=self.sign_up_data['email'])
|
|
||||||
# # Invalidate all other links for this user
|
|
||||||
# old_acs = AccountConfirmation.objects.filter(
|
|
||||||
# user=user)
|
|
||||||
# for old_ac in old_acs:
|
|
||||||
# old_ac.confirmed = True
|
|
||||||
# old_ac.save()
|
|
||||||
# # Create Account Confirmation UUID
|
|
||||||
# new_ac = AccountConfirmation.objects.create(user=user)
|
|
||||||
# self.assertFalse(new_ac.is_expired)
|
|
||||||
# on_user_confirm_resend.send(
|
|
||||||
# sender=None,
|
|
||||||
# user=user,
|
|
||||||
# request=None)
|
|
||||||
|
|
||||||
# def test_reset_passowrd(self):
|
|
||||||
# """Test reset password POST"""
|
|
||||||
# # Signup user first
|
|
||||||
# sign_up_form = SignUpForm(self.sign_up_data)
|
|
||||||
# self.assertTrue(sign_up_form.is_valid())
|
|
||||||
|
|
||||||
# sign_up_res = test_request(accounts.SignUpView.as_view(),
|
|
||||||
# method='POST',
|
|
||||||
# req_kwargs=sign_up_form.cleaned_data)
|
|
||||||
# self.assertEqual(sign_up_res.status_code, 302)
|
|
||||||
|
|
||||||
# user = User.objects.get(email=self.sign_up_data['email'])
|
|
||||||
# # Invalidate all other links for this user
|
|
||||||
# old_acs = AccountConfirmation.objects.filter(
|
|
||||||
# user=user)
|
|
||||||
# for old_ac in old_acs:
|
|
||||||
# old_ac.confirmed = True
|
|
||||||
# old_ac.save()
|
|
||||||
# # Create Account Confirmation UUID
|
|
||||||
# new_ac = AccountConfirmation.objects.create(user=user)
|
|
||||||
# self.assertFalse(new_ac.is_expired)
|
|
||||||
# uuid = AccountConfirmation.objects.filter(user=user).first().pk
|
|
||||||
# reset_res = test_request(accounts.PasswordResetFinishView.as_view(),
|
|
||||||
# method='POST',
|
|
||||||
# user=user,
|
|
||||||
# url_kwargs={'uuid': uuid},
|
|
||||||
# req_kwargs=self.change_data)
|
|
||||||
|
|
||||||
# self.assertEqual(reset_res.status_code, 302)
|
|
||||||
# self.assertEqual(reset_res.url, reverse('common-index'))
|
|
||||||
|
|
|
@ -13,14 +13,13 @@ urlpatterns = [
|
||||||
name="auth-login",
|
name="auth-login",
|
||||||
),
|
),
|
||||||
path("auth/logout/", authentication.LogoutView.as_view(), name="auth-logout"),
|
path("auth/logout/", authentication.LogoutView.as_view(), name="auth-logout"),
|
||||||
path("auth/sign_up/", authentication.SignUpView.as_view(), name="auth-sign-up"),
|
|
||||||
path(
|
path(
|
||||||
"auth/sign_up/<uuid:nonce>/confirm/",
|
"auth/sign_up/",
|
||||||
authentication.SignUpConfirmView.as_view(),
|
ToDefaultFlow.as_view(designation=FlowDesignation.ENROLLMENT),
|
||||||
name="auth-sign-up-confirm",
|
name="auth-sign-up",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"auth/password/reset/<uuid:nonce>/",
|
"auth/password/reset/<uuid:nonce_uuid>/",
|
||||||
authentication.PasswordResetView.as_view(),
|
authentication.PasswordResetView.as_view(),
|
||||||
name="auth-password-reset",
|
name="auth-password-reset",
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,22 +1,14 @@
|
||||||
"""passbook core authentication views"""
|
"""passbook core authentication views"""
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import login, logout
|
from django.contrib.auth import login, logout
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.forms.utils import ErrorList
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import FormView
|
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.forms.authentication import SignUpForm
|
from passbook.core.models import Nonce
|
||||||
from passbook.core.models import Invitation, Nonce, User
|
|
||||||
from passbook.core.signals import invitation_used, user_signed_up
|
|
||||||
from passbook.lib.config import CONFIG
|
|
||||||
from passbook.stages.password.exceptions import PasswordPolicyInvalid
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -24,146 +16,20 @@ LOGGER = get_logger()
|
||||||
class LogoutView(LoginRequiredMixin, View):
|
class LogoutView(LoginRequiredMixin, View):
|
||||||
"""Log current user out"""
|
"""Log current user out"""
|
||||||
|
|
||||||
def dispatch(self, request):
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
"""Log current user out"""
|
"""Log current user out"""
|
||||||
logout(request)
|
logout(request)
|
||||||
messages.success(request, _("You've successfully been logged out."))
|
messages.success(request, _("You've successfully been logged out."))
|
||||||
return redirect(reverse("passbook_core:auth-login"))
|
return redirect(reverse("passbook_core:auth-login"))
|
||||||
|
|
||||||
|
|
||||||
class SignUpView(UserPassesTestMixin, FormView):
|
|
||||||
"""Sign up new user, optionally consume one-use invitation link."""
|
|
||||||
|
|
||||||
template_name = "login/form.html"
|
|
||||||
form_class = SignUpForm
|
|
||||||
success_url = "."
|
|
||||||
# Invitation instance, if invitation link was used
|
|
||||||
_invitation = None
|
|
||||||
# Instance of newly created user
|
|
||||||
_user = None
|
|
||||||
|
|
||||||
# Allow only not authenticated users to login
|
|
||||||
def test_func(self):
|
|
||||||
return self.request.user.is_authenticated is False
|
|
||||||
|
|
||||||
def handle_no_permission(self):
|
|
||||||
return redirect(reverse("passbook_core:overview"))
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
"""Check if sign-up is enabled or invitation link given"""
|
|
||||||
allowed = False
|
|
||||||
if "invitation" in request.GET:
|
|
||||||
invitations = Invitation.objects.filter(uuid=request.GET.get("invitation"))
|
|
||||||
allowed = invitations.exists()
|
|
||||||
if allowed:
|
|
||||||
self._invitation = invitations.first()
|
|
||||||
if CONFIG.y("passbook.sign_up.enabled"):
|
|
||||||
allowed = True
|
|
||||||
if not allowed:
|
|
||||||
messages.error(request, _("Sign-ups are currently disabled."))
|
|
||||||
return redirect(reverse("passbook_core:auth-login"))
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
if self._invitation:
|
|
||||||
initial = {}
|
|
||||||
if self._invitation.fixed_username:
|
|
||||||
initial["username"] = self._invitation.fixed_username
|
|
||||||
if self._invitation.fixed_email:
|
|
||||||
initial["email"] = self._invitation.fixed_email
|
|
||||||
return initial
|
|
||||||
return super().get_initial()
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
kwargs["config"] = CONFIG.y("passbook")
|
|
||||||
kwargs["title"] = _("Sign Up")
|
|
||||||
kwargs["primary_action"] = _("Sign up")
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
def form_valid(self, form: SignUpForm) -> HttpResponse:
|
|
||||||
"""Create user"""
|
|
||||||
try:
|
|
||||||
self._user = SignUpView.create_user(form.cleaned_data, self.request)
|
|
||||||
except PasswordPolicyInvalid as exc:
|
|
||||||
# Manually inject error into form
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
errors = form._errors.setdefault("password", ErrorList())
|
|
||||||
for error in exc.messages:
|
|
||||||
errors.append(error)
|
|
||||||
return self.form_invalid(form)
|
|
||||||
self.consume_invitation()
|
|
||||||
messages.success(self.request, _("Successfully signed up!"))
|
|
||||||
LOGGER.debug("Successfully signed up", email=form.cleaned_data.get("email"))
|
|
||||||
return redirect(reverse("passbook_core:auth-login"))
|
|
||||||
|
|
||||||
def consume_invitation(self):
|
|
||||||
"""Consume invitation if an invitation was used"""
|
|
||||||
if self._invitation:
|
|
||||||
invitation_used.send(
|
|
||||||
sender=self,
|
|
||||||
request=self.request,
|
|
||||||
invitation=self._invitation,
|
|
||||||
user=self._user,
|
|
||||||
)
|
|
||||||
self._invitation.delete()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_user(data: Dict, request: HttpRequest = None) -> User:
|
|
||||||
"""Create user from data
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Dictionary as returned by SignUpForm's cleaned_data
|
|
||||||
request: Optional current request.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The user created
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
PasswordPolicyInvalid: if any policy are not fulfilled.
|
|
||||||
This also deletes the created user.
|
|
||||||
"""
|
|
||||||
# Create user
|
|
||||||
new_user = User.objects.create(
|
|
||||||
username=data.get("username"),
|
|
||||||
email=data.get("email"),
|
|
||||||
name=data.get("name"),
|
|
||||||
)
|
|
||||||
new_user.is_active = True
|
|
||||||
try:
|
|
||||||
new_user.set_password(data.get("password"))
|
|
||||||
new_user.save()
|
|
||||||
request.user = new_user
|
|
||||||
# Send signal for other auth sources
|
|
||||||
user_signed_up.send(sender=SignUpView, user=new_user, request=request)
|
|
||||||
return new_user
|
|
||||||
except PasswordPolicyInvalid as exc:
|
|
||||||
new_user.delete()
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
|
|
||||||
class SignUpConfirmView(View):
|
|
||||||
"""Confirm registration from Nonce"""
|
|
||||||
|
|
||||||
def get(self, request, nonce):
|
|
||||||
"""Verify UUID and activate user"""
|
|
||||||
nonce = get_object_or_404(Nonce, uuid=nonce)
|
|
||||||
nonce.user.is_active = True
|
|
||||||
nonce.user.save()
|
|
||||||
# Workaround: hardcoded reference to ModelBackend, needs testing
|
|
||||||
nonce.user.backend = "django.contrib.auth.backends.ModelBackend"
|
|
||||||
login(request, nonce.user)
|
|
||||||
nonce.delete()
|
|
||||||
messages.success(request, _("Successfully confirmed registration."))
|
|
||||||
return redirect("passbook_core:overview")
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetView(View):
|
class PasswordResetView(View):
|
||||||
"""Temporarily authenticate User and allow them to reset their password"""
|
"""Temporarily authenticate User and allow them to reset their password"""
|
||||||
|
|
||||||
def get(self, request, nonce):
|
def get(self, request: HttpRequest, nonce_uuid: str) -> HttpResponse:
|
||||||
"""Authenticate user with nonce and redirect to password change view"""
|
"""Authenticate user with nonce and redirect to password change view"""
|
||||||
# 3. (Optional) Trap user in password change view
|
# 3. (Optional) Trap user in password change view
|
||||||
nonce = get_object_or_404(Nonce, uuid=nonce)
|
nonce = get_object_or_404(Nonce, uuid=nonce_uuid)
|
||||||
# Workaround: hardcoded reference to ModelBackend, needs testing
|
# Workaround: hardcoded reference to ModelBackend, needs testing
|
||||||
nonce.user.backend = "django.contrib.auth.backends.ModelBackend"
|
nonce.user.backend = "django.contrib.auth.backends.ModelBackend"
|
||||||
login(request, nonce.user)
|
login(request, nonce.user)
|
||||||
|
|
|
@ -79,37 +79,38 @@ INSTALLED_APPS = [
|
||||||
"drf_yasg",
|
"drf_yasg",
|
||||||
"guardian",
|
"guardian",
|
||||||
"django_prometheus",
|
"django_prometheus",
|
||||||
"passbook.static.apps.PassbookStaticConfig",
|
|
||||||
"passbook.admin.apps.PassbookAdminConfig",
|
"passbook.admin.apps.PassbookAdminConfig",
|
||||||
"passbook.api.apps.PassbookAPIConfig",
|
"passbook.api.apps.PassbookAPIConfig",
|
||||||
"passbook.lib.apps.PassbookLibConfig",
|
|
||||||
"passbook.flows.apps.PassbookFlowsConfig",
|
|
||||||
"passbook.policies.apps.PassbookPoliciesConfig",
|
|
||||||
"passbook.audit.apps.PassbookAuditConfig",
|
"passbook.audit.apps.PassbookAuditConfig",
|
||||||
"passbook.crypto.apps.PassbookCryptoConfig",
|
"passbook.crypto.apps.PassbookCryptoConfig",
|
||||||
"passbook.recovery.apps.PassbookRecoveryConfig",
|
"passbook.flows.apps.PassbookFlowsConfig",
|
||||||
"passbook.sources.saml.apps.PassbookSourceSAMLConfig",
|
"passbook.lib.apps.PassbookLibConfig",
|
||||||
"passbook.sources.ldap.apps.PassbookSourceLDAPConfig",
|
"passbook.policies.apps.PassbookPoliciesConfig",
|
||||||
"passbook.sources.oauth.apps.PassbookSourceOAuthConfig",
|
"passbook.policies.dummy.apps.PassbookPolicyDummyConfig",
|
||||||
|
"passbook.policies.expiry.apps.PassbookPolicyExpiryConfig",
|
||||||
|
"passbook.policies.expression.apps.PassbookPolicyExpressionConfig",
|
||||||
|
"passbook.policies.hibp.apps.PassbookPolicyHIBPConfig",
|
||||||
|
"passbook.policies.password.apps.PassbookPoliciesPasswordConfig",
|
||||||
|
"passbook.policies.reputation.apps.PassbookPolicyReputationConfig",
|
||||||
|
"passbook.policies.webhook.apps.PassbookPoliciesWebhookConfig",
|
||||||
"passbook.providers.app_gw.apps.PassbookApplicationApplicationGatewayConfig",
|
"passbook.providers.app_gw.apps.PassbookApplicationApplicationGatewayConfig",
|
||||||
"passbook.providers.oauth.apps.PassbookProviderOAuthConfig",
|
"passbook.providers.oauth.apps.PassbookProviderOAuthConfig",
|
||||||
"passbook.providers.oidc.apps.PassbookProviderOIDCConfig",
|
"passbook.providers.oidc.apps.PassbookProviderOIDCConfig",
|
||||||
"passbook.providers.saml.apps.PassbookProviderSAMLConfig",
|
"passbook.providers.saml.apps.PassbookProviderSAMLConfig",
|
||||||
"passbook.providers.samlv2.apps.PassbookProviderSAMLv2Config",
|
"passbook.providers.samlv2.apps.PassbookProviderSAMLv2Config",
|
||||||
|
"passbook.recovery.apps.PassbookRecoveryConfig",
|
||||||
|
"passbook.sources.ldap.apps.PassbookSourceLDAPConfig",
|
||||||
|
"passbook.sources.oauth.apps.PassbookSourceOAuthConfig",
|
||||||
|
"passbook.sources.saml.apps.PassbookSourceSAMLConfig",
|
||||||
|
"passbook.stages.captcha.apps.PassbookStageCaptchaConfig",
|
||||||
"passbook.stages.dummy.apps.PassbookStageDummyConfig",
|
"passbook.stages.dummy.apps.PassbookStageDummyConfig",
|
||||||
"passbook.stages.login.apps.PassbookStageLoginConfig",
|
"passbook.stages.login.apps.PassbookStageLoginConfig",
|
||||||
|
"passbook.stages.email.apps.PassbookStageEmailConfig",
|
||||||
|
"passbook.stages.prompt.apps.PassbookStagPromptConfig",
|
||||||
"passbook.stages.identification.apps.PassbookStageIdentificationConfig",
|
"passbook.stages.identification.apps.PassbookStageIdentificationConfig",
|
||||||
"passbook.stages.otp.apps.PassbookStageOTPConfig",
|
"passbook.stages.otp.apps.PassbookStageOTPConfig",
|
||||||
"passbook.stages.captcha.apps.PassbookStageCaptchaConfig",
|
|
||||||
"passbook.stages.password.apps.PassbookStagePasswordConfig",
|
"passbook.stages.password.apps.PassbookStagePasswordConfig",
|
||||||
"passbook.stages.email.apps.PassbookStageEmailConfig",
|
"passbook.static.apps.PassbookStaticConfig",
|
||||||
"passbook.policies.dummy.apps.PassbookPolicyDummyConfig",
|
|
||||||
"passbook.policies.expiry.apps.PassbookPolicyExpiryConfig",
|
|
||||||
"passbook.policies.reputation.apps.PassbookPolicyReputationConfig",
|
|
||||||
"passbook.policies.hibp.apps.PassbookPolicyHIBPConfig",
|
|
||||||
"passbook.policies.password.apps.PassbookPoliciesPasswordConfig",
|
|
||||||
"passbook.policies.webhook.apps.PassbookPoliciesWebhookConfig",
|
|
||||||
"passbook.policies.expression.apps.PassbookPolicyExpressionConfig",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
GUARDIAN_MONKEY_PATCH = False
|
GUARDIAN_MONKEY_PATCH = False
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
"""Prompt Stage API Views"""
|
||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from passbook.stages.prompt.models import Prompt, PromptStage
|
||||||
|
|
||||||
|
|
||||||
|
class PromptStageSerializer(ModelSerializer):
|
||||||
|
"""PromptStage Serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = PromptStage
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"name",
|
||||||
|
"fields",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PromptStageViewSet(ModelViewSet):
|
||||||
|
"""PromptStage Viewset"""
|
||||||
|
|
||||||
|
queryset = PromptStage.objects.all()
|
||||||
|
serializer_class = PromptStageSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PromptSerializer(ModelSerializer):
|
||||||
|
"""Prompt Serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = Prompt
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"field_key",
|
||||||
|
"label",
|
||||||
|
"type",
|
||||||
|
"required",
|
||||||
|
"placeholder",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PromptViewSet(ModelViewSet):
|
||||||
|
"""Prompt Viewset"""
|
||||||
|
|
||||||
|
queryset = Prompt.objects.all()
|
||||||
|
serializer_class = PromptSerializer
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""passbook prompt stage app config"""
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PassbookStagPromptConfig(AppConfig):
|
||||||
|
"""passbook prompt stage config"""
|
||||||
|
|
||||||
|
name = "passbook.stages.prompt"
|
||||||
|
label = "passbook_stages_prompt"
|
||||||
|
verbose_name = "passbook Stages.Prompt"
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Prompt forms"""
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from passbook.stages.prompt.models import Prompt, PromptStage
|
||||||
|
|
||||||
|
|
||||||
|
class PromptStageForm(forms.ModelForm):
|
||||||
|
"""Form to create/edit Prompt Stage instances"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = PromptStage
|
||||||
|
fields = ["name", "fields"]
|
||||||
|
widgets = {
|
||||||
|
"name": forms.TextInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PromptForm(forms.Form):
|
||||||
|
"""Dynamically created form based on PromptStage"""
|
||||||
|
|
||||||
|
stage: PromptStage
|
||||||
|
|
||||||
|
def __init__(self, stage: PromptStage, *args, **kwargs):
|
||||||
|
self.stage = stage
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for field in self.stage.fields.all():
|
||||||
|
field: Prompt
|
||||||
|
self.fields[field.field_key] = field.field
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-05-10 14:03
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_flows", "0003_auto_20200509_1258"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Prompt",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"field_key",
|
||||||
|
models.SlugField(
|
||||||
|
help_text="Name of the form field, also used to store the value"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("label", models.TextField()),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("text", "Text"),
|
||||||
|
("e-mail", "Email"),
|
||||||
|
("password", "Password"),
|
||||||
|
("number", "Number"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("required", models.BooleanField(default=True)),
|
||||||
|
("placeholder", models.TextField()),
|
||||||
|
],
|
||||||
|
options={"abstract": False,},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PromptStage",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"stage_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="passbook_flows.Stage",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("fields", models.ManyToManyField(to="passbook_stages_prompt.Prompt")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Prompt Stage",
|
||||||
|
"verbose_name_plural": "Prompt Stages",
|
||||||
|
},
|
||||||
|
bases=("passbook_flows.stage",),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,75 @@
|
||||||
|
"""prompt models"""
|
||||||
|
from django import forms
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from passbook.flows.models import Stage
|
||||||
|
from passbook.lib.models import UUIDModel
|
||||||
|
|
||||||
|
|
||||||
|
class FieldTypes(models.TextChoices):
|
||||||
|
"""Field types an Prompt can be"""
|
||||||
|
|
||||||
|
TEXT = "text"
|
||||||
|
EMAIL = "e-mail"
|
||||||
|
PASSWORD = "password" # noqa # nosec
|
||||||
|
NUMBER = "number"
|
||||||
|
|
||||||
|
|
||||||
|
class Prompt(UUIDModel):
|
||||||
|
"""Single Prompt, part of a prompt stage."""
|
||||||
|
|
||||||
|
field_key = models.SlugField(
|
||||||
|
help_text=_("Name of the form field, also used to store the value")
|
||||||
|
)
|
||||||
|
label = models.TextField()
|
||||||
|
type = models.CharField(max_length=100, choices=FieldTypes.choices)
|
||||||
|
required = models.BooleanField(default=True)
|
||||||
|
placeholder = models.TextField()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def field(self):
|
||||||
|
"""Return instantiated form input field"""
|
||||||
|
attrs = {"placeholder": _(self.placeholder)}
|
||||||
|
if self.type == FieldTypes.TEXT:
|
||||||
|
return forms.CharField(
|
||||||
|
label=_(self.label),
|
||||||
|
widget=forms.TextInput(attrs=attrs),
|
||||||
|
required=self.required,
|
||||||
|
)
|
||||||
|
if self.type == FieldTypes.EMAIL:
|
||||||
|
return forms.EmailField(
|
||||||
|
label=_(self.label),
|
||||||
|
widget=forms.TextInput(attrs=attrs),
|
||||||
|
required=self.required,
|
||||||
|
)
|
||||||
|
if self.type == FieldTypes.PASSWORD:
|
||||||
|
return forms.CharField(
|
||||||
|
label=_(self.label),
|
||||||
|
widget=forms.PasswordInput(attrs=attrs),
|
||||||
|
required=self.required,
|
||||||
|
)
|
||||||
|
if self.type == FieldTypes.NUMBER:
|
||||||
|
return forms.IntegerField(
|
||||||
|
label=_(self.label),
|
||||||
|
widget=forms.NumberInput(attrs=attrs),
|
||||||
|
requred=self.required,
|
||||||
|
)
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
class PromptStage(Stage):
|
||||||
|
"""Prompt Stage, pointing to multiple prompts"""
|
||||||
|
|
||||||
|
fields = models.ManyToManyField(Prompt)
|
||||||
|
|
||||||
|
type = "passbook.stages.prompt.stage.PromptStageView"
|
||||||
|
form = "passbook.stages.prompt.forms.PromptStageForm"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Prompt Stage {self.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
verbose_name = _("Prompt Stage")
|
||||||
|
verbose_name_plural = _("Prompt Stages")
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""Enrollment Stage Logic"""
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.generic import FormView
|
||||||
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.flows.stage import AuthenticationStage
|
||||||
|
from passbook.stages.prompt.forms import PromptForm
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
PLAN_CONTEXT_PROMPT = "prompt_data"
|
||||||
|
|
||||||
|
|
||||||
|
class EnrollmentStageView(FormView, AuthenticationStage):
|
||||||
|
"""Enrollment Stage, save form data in plan context."""
|
||||||
|
|
||||||
|
template_name = "login/form.html"
|
||||||
|
form_class = PromptForm
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx["title"] = _(self.executor.current_stage.name)
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs["stage"] = self.executor.current_stage
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, form: PromptForm) -> HttpResponse:
|
||||||
|
"""Form data is valid"""
|
||||||
|
if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
|
||||||
|
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = {}
|
||||||
|
self.executor.plan.context[PLAN_CONTEXT_PROMPT].update(form.cleaned_data)
|
||||||
|
return self.executor.stage_ok()
|
Reference in New Issue