diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py index d651e991f2..2a66dca995 100644 --- a/passbook/api/v2/urls.py +++ b/passbook/api/v2/urls.py @@ -33,6 +33,8 @@ from passbook.sources.oauth.api import OAuthSourceViewSet from passbook.stages.captcha.api import CaptchaStageViewSet from passbook.stages.dummy.api import DummyStageViewSet from passbook.stages.email.api import EmailStageViewSet +from passbook.stages.identification.api import IdentificationStageViewSet +from passbook.stages.login.api import LoginStageViewSet from passbook.stages.otp.api import OTPStageViewSet from passbook.stages.password.api import PasswordStageViewSet @@ -49,10 +51,13 @@ router.register("core/applications", ApplicationViewSet) router.register("core/invitations", InvitationViewSet) router.register("core/groups", GroupViewSet) router.register("core/users", UserViewSet) + router.register("audit/events", EventViewSet) + router.register("sources/all", SourceViewSet) router.register("sources/ldap", LDAPSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet) + router.register("policies/all", PolicyViewSet) router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet) router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet) @@ -60,20 +65,26 @@ router.register("policies/password", PasswordPolicyViewSet) router.register("policies/reputation", ReputationPolicyViewSet) router.register("policies/webhook", WebhookPolicyViewSet) router.register("policies/expression", ExpressionPolicyViewSet) + router.register("providers/all", ProviderViewSet) router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet) router.register("providers/oauth", OAuth2ProviderViewSet) router.register("providers/openid", OpenIDProviderViewSet) router.register("providers/saml", SAMLProviderViewSet) + router.register("propertymappings/all", PropertyMappingViewSet) router.register("propertymappings/ldap", LDAPPropertyMappingViewSet) router.register("propertymappings/saml", SAMLPropertyMappingViewSet) + router.register("stages/all", StageViewSet) router.register("stages/captcha", CaptchaStageViewSet) router.register("stages/dummy", DummyStageViewSet) router.register("stages/email", EmailStageViewSet) router.register("stages/otp", OTPStageViewSet) router.register("stages/password", PasswordStageViewSet) +router.register("stages/identification", IdentificationStageViewSet) +router.register("stages/login", LoginStageViewSet) + router.register("flows", FlowViewSet) router.register("flows/bindings", FlowStageBindingViewSet) diff --git a/passbook/flows/migrations/0002_default_flows.py b/passbook/flows/migrations/0002_default_flows.py index 18812ad759..556cfaa3c4 100644 --- a/passbook/flows/migrations/0002_default_flows.py +++ b/passbook/flows/migrations/0002_default_flows.py @@ -12,6 +12,9 @@ def create_default_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): Flow = apps.get_model("passbook_flows", "Flow") FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding") PasswordStage = apps.get_model("passbook_stages_password", "PasswordStage") + LoginStage = apps.get_model( + "passbook_stages_login", "LoginStage" + ) IdentificationStage = apps.get_model( "passbook_stages_identification", "IdentificationStage" ) @@ -33,8 +36,12 @@ def create_default_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): name="password", backends=["django.contrib.auth.backends.ModelBackend"], ) + if not LoginStage.objects.using(db_alias).exists(): + LoginStage.objects.using(db_alias).create(name="authentication") + ident_stage = IdentificationStage.objects.using(db_alias).first() pw_stage = PasswordStage.objects.using(db_alias).first() + login_stage = LoginStage.objects.using(db_alias).first() flow = Flow.objects.using(db_alias).create( name="default-authentication-flow", slug="default-authentication-flow", @@ -46,12 +53,16 @@ def create_default_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): FlowStageBinding.objects.using(db_alias).create( flow=flow, stage=pw_stage, order=1, ) + FlowStageBinding.objects.using(db_alias).create( + flow=flow, stage=login_stage, order=2, + ) class Migration(migrations.Migration): dependencies = [ ("passbook_flows", "0001_initial"), + ("passbook_stages_login", "0001_initial"), ("passbook_stages_password", "0001_initial"), ("passbook_stages_identification", "0001_initial"), ] diff --git a/passbook/flows/views.py b/passbook/flows/views.py index f4a3805db5..eaa3b7e975 100644 --- a/passbook/flows/views.py +++ b/passbook/flows/views.py @@ -1,7 +1,6 @@ """passbook multi-stage authentication engine""" from typing import Optional -from django.contrib.auth import login from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.views.generic import View @@ -27,7 +26,7 @@ class FlowExecutorView(View): flow: Flow - plan: FlowPlan + plan: Optional[FlowPlan] = None current_stage: Stage current_stage_view: View @@ -116,15 +115,6 @@ class FlowExecutorView(View): def _flow_done(self) -> HttpResponse: """User Successfully passed all stages""" - backend = self.plan.context[PLAN_CONTEXT_PENDING_USER].backend - login( - self.request, self.plan.context[PLAN_CONTEXT_PENDING_USER], backend=backend - ) - LOGGER.debug( - "Logged in", - user=self.plan.context[PLAN_CONTEXT_PENDING_USER], - flow_slug=self.flow.slug, - ) self.cancel() next_param = self.request.GET.get(NEXT_ARG_NAME, None) if next_param and not is_url_absolute(next_param): @@ -165,10 +155,9 @@ class FlowExecutorView(View): self.cancel() return redirect_with_qs("passbook_flows:denied", self.request.GET) - def cancel(self) -> HttpResponse: + def cancel(self): """Cancel current execution and return a redirect""" del self.request.session[SESSION_KEY_PLAN] - return redirect_with_qs("passbook_flows:denied", self.request.GET) class FlowPermissionDeniedView(PermissionDeniedView): diff --git a/passbook/stages/login/__init__.py b/passbook/stages/login/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/passbook/stages/login/api.py b/passbook/stages/login/api.py new file mode 100644 index 0000000000..eb28d9042c --- /dev/null +++ b/passbook/stages/login/api.py @@ -0,0 +1,24 @@ +"""Login Stage API Views""" +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from passbook.stages.login.models import LoginStage + + +class LoginStageSerializer(ModelSerializer): + """LoginStage Serializer""" + + class Meta: + + model = LoginStage + fields = [ + "pk", + "name", + ] + + +class LoginStageViewSet(ModelViewSet): + """LoginStage Viewset""" + + queryset = LoginStage.objects.all() + serializer_class = LoginStageSerializer diff --git a/passbook/stages/login/apps.py b/passbook/stages/login/apps.py new file mode 100644 index 0000000000..83ec4e638a --- /dev/null +++ b/passbook/stages/login/apps.py @@ -0,0 +1,10 @@ +"""passbook login stage app config""" +from django.apps import AppConfig + + +class PassbookStageLoginConfig(AppConfig): + """passbook login stage config""" + + name = "passbook.stages.login" + label = "passbook_stages_login" + verbose_name = "passbook Stages.Login" diff --git a/passbook/stages/login/forms.py b/passbook/stages/login/forms.py new file mode 100644 index 0000000000..5dc8d6ee5d --- /dev/null +++ b/passbook/stages/login/forms.py @@ -0,0 +1,16 @@ +"""passbook flows login forms""" +from django import forms + +from passbook.stages.login.models import LoginStage + + +class LoginStageForm(forms.ModelForm): + """Form to create/edit LoginStage instances""" + + class Meta: + + model = LoginStage + fields = ["name"] + widgets = { + "name": forms.TextInput(), + } diff --git a/passbook/stages/login/migrations/0001_initial.py b/passbook/stages/login/migrations/0001_initial.py new file mode 100644 index 0000000000..52e6060d81 --- /dev/null +++ b/passbook/stages/login/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 3.0.3 on 2020-05-09 20:26 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("passbook_flows", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="LoginStage", + 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", + ), + ), + ], + options={ + "verbose_name": "Login Stage", + "verbose_name_plural": "Login Stages", + }, + bases=("passbook_flows.stage",), + ), + ] diff --git a/passbook/stages/login/migrations/__init__.py b/passbook/stages/login/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/passbook/stages/login/models.py b/passbook/stages/login/models.py new file mode 100644 index 0000000000..b48f0566e8 --- /dev/null +++ b/passbook/stages/login/models.py @@ -0,0 +1,19 @@ +"""login stage models""" +from django.utils.translation import gettext_lazy as _ + +from passbook.flows.models import Stage + + +class LoginStage(Stage): + """Login stage, allows a user to identify themselves to authenticate.""" + + type = "passbook.stages.login.stage.LoginStageView" + form = "passbook.stages.login.forms.LoginStageForm" + + def __str__(self): + return f"Login Stage {self.name}" + + class Meta: + + verbose_name = _("Login Stage") + verbose_name_plural = _("Login Stages") diff --git a/passbook/stages/login/stage.py b/passbook/stages/login/stage.py new file mode 100644 index 0000000000..74bce930bb --- /dev/null +++ b/passbook/stages/login/stage.py @@ -0,0 +1,36 @@ +"""Login stage logic""" +from django.contrib import messages +from django.contrib.auth import login +from django.http import HttpRequest, HttpResponse +from django.utils.translation import gettext as _ +from structlog import get_logger + +from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER +from passbook.flows.stage import AuthenticationStage +from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND + +LOGGER = get_logger() + + +class LoginStageView(AuthenticationStage): + """Finalise Authentication flow by logging the user in""" + + def get(self, request: HttpRequest) -> HttpResponse: + if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context: + messages.error(request, _("No Pending user to login.")) + return self.executor.stage_invalid() + if PLAN_CONTEXT_AUTHENTICATION_BACKEND not in self.executor.plan.context: + messages.error(request, _("Pending user has no backend.")) + return self.executor.stage_invalid() + backend = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER].backend + login( + self.request, + self.executor.plan.context[PLAN_CONTEXT_PENDING_USER], + backend=backend, + ) + LOGGER.debug( + "Logged in", + user=self.executor.plan.context[PLAN_CONTEXT_PENDING_USER], + flow_slug=self.executor.flow.slug, + ) + return self.executor.stage_ok()