Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions passbook/admin/templates/administration/flow/list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{% extends "administration/base.html" %}

{% load i18n %}
{% load utils %}

{% block content %}
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-flows"></i>
{% trans 'Flows' %}
</h1>
<p>{% trans "External Flows which use passbook as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}</p>
</div>
</section>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
<div class="pf-c-toolbar__action-group">
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
</div>
{% include 'partials/pagination.html' %}
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Identifier' %}</th>
<th role="columnheader" scope="col">{% trans 'Designation' %}</th>
<th role="columnheader" scope="col">{% trans 'Bindings' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for flow in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ flow.slug }}</div>
</div>
</th>
<td role="cell">
<span>
{{ flow.designation }}
</span>
</td>
<td role="cell">
<span>
{{ flow.factors.all|length }}
</span>
</td>
<td>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-update' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:flow-delete' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
{% include 'partials/pagination.html' %}
</div>
</div>
</section>
{% endblock %}
10 changes: 10 additions & 0 deletions passbook/admin/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
audit,
debug,
factors,
flows,
groups,
invitations,
overview,
Expand Down Expand Up @@ -37,6 +38,15 @@
applications.ApplicationDeleteView.as_view(),
name="application-delete",
),
# Flows
path("flows/", flows.FlowListView.as_view(), name="flows"),
path("flows/create/", flows.FlowCreateView.as_view(), name="flow-create",),
path(
"flows/<uuid:pk>/update/", flows.FlowUpdateView.as_view(), name="flow-update",
),
path(
"flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
),
# Sources
path("sources/", sources.SourceListView.as_view(), name="sources"),
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
Expand Down
2 changes: 1 addition & 1 deletion passbook/admin/views/factors.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView):
model = Factor
template_name = "administration/factor/list.html"
permission_required = "passbook_core.view_factor"
ordering = "order"
ordering = "slug"
paginate_by = 40

def get_context_data(self, **kwargs):
Expand Down
77 changes: 77 additions & 0 deletions passbook/admin/views/flows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""passbook Flow administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin

from passbook.flows.forms import FlowForm
from passbook.flows.models import Flow
from passbook.lib.views import CreateAssignPermView


class FlowListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all flows"""

model = Flow
permission_required = "passbook_flows.view_flow"
ordering = "slug"
paginate_by = 40
template_name = "administration/flow/list.html"


class FlowCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
):
"""Create new Flow"""

model = Flow
form_class = FlowForm
permission_required = "passbook_flows.add_flow"

template_name = "generic/create.html"
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully created Flow")

def get_context_data(self, **kwargs):
kwargs["type"] = "Flow"
return super().get_context_data(**kwargs)


class FlowUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update flow"""

model = Flow
form_class = FlowForm
permission_required = "passbook_flows.change_flow"

template_name = "generic/update.html"
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully updated Flow")


class FlowDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete flow"""

model = Flow
permission_required = "passbook_flows.delete_flow"

template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully deleted Flow")

def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
2 changes: 2 additions & 0 deletions passbook/api/v2/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from passbook.factors.email.api import EmailFactorViewSet
from passbook.factors.otp.api import OTPFactorViewSet
from passbook.factors.password.api import PasswordFactorViewSet
from passbook.flows.api.execute import FlowsExecuteViewSet
from passbook.lib.utils.reflection import get_apps
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
from passbook.policies.expression.api import ExpressionPolicyViewSet
Expand Down Expand Up @@ -74,6 +75,7 @@
router.register("factors/email", EmailFactorViewSet)
router.register("factors/otp", OTPFactorViewSet)
router.register("factors/password", PasswordFactorViewSet)
router.register("flows/execute", FlowsExecuteViewSet, basename="flow")

info = openapi.Info(
title="passbook API",
Expand Down
15 changes: 15 additions & 0 deletions passbook/core/migrations/0012_auto_20200303_1530.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Generated by Django 3.0.3 on 2020-03-03 15:30

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("passbook_core", "0011_auto_20200222_1822"),
]

operations = [
migrations.RemoveField(model_name="factor", name="enabled",),
migrations.RemoveField(model_name="factor", name="order",),
]
11 changes: 9 additions & 2 deletions passbook/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from passbook.core.signals import password_changed
from passbook.core.types import UILoginButton, UIUserSettings
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.policies.exceptions import PolicyException
from passbook.policies.types import PolicyRequest, PolicyResult

Expand Down Expand Up @@ -110,13 +111,19 @@ class Factor(ExportModelOperationsMixin("factor"), PolicyModel):
slug = models.SlugField(
unique=True, help_text=_("Internal factor name, used in URLs.")
)
order = models.IntegerField()
enabled = models.BooleanField(default=True)

objects = InheritanceManager()
type = ""
form = ""

_factor_class = None

@property
def factor_class(self):
if not self._factor_class:
self._factor_class = path_to_class(self.type)
return self._factor_class

@property
def ui_user_settings(self) -> Optional[UIUserSettings]:
"""Entrypoint to integrate with User settings. Can either return None if no
Expand Down
2 changes: 1 addition & 1 deletion passbook/core/templates/login/factors/backend.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

{% block beneath_form %}
{% if show_password_forget_notice %}
<a href="{% url 'passbook_core:auth-process' %}?password-forgotten">{% trans 'Forgot password?' %}</a>
<a href="{% url 'passbook_core:flows-execute' %}?password-forgotten">{% trans 'Forgot password?' %}</a>
{% endif %}
{% endblock %}
2 changes: 1 addition & 1 deletion passbook/core/tests/test_views_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def test_login_view_post(self):
reverse("passbook_core:auth-login"), data=self.login_data
)
self.assertEqual(login_response.status_code, 302)
self.assertEqual(login_response.url, reverse("passbook_core:auth-process"))
self.assertEqual(login_response.url, reverse("passbook_core:flows-execute"))

def test_sign_up_view_post(self):
"""Test account.sign_up view POST (Anonymous)"""
Expand Down
11 changes: 3 additions & 8 deletions passbook/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from structlog import get_logger

from passbook.core.views import authentication, overview, user
from passbook.factors import view
from passbook.flows.executor.http import FactorPermissionDeniedView, HttpExecutorView

LOGGER = get_logger()

Expand All @@ -19,20 +19,15 @@
),
path(
"auth/process/denied/",
view.FactorPermissionDeniedView.as_view(),
FactorPermissionDeniedView.as_view(),
name="auth-denied",
),
path(
"auth/password/reset/<uuid:nonce>/",
authentication.PasswordResetView.as_view(),
name="auth-password-reset",
),
path("auth/process/", view.AuthenticationView.as_view(), name="auth-process"),
path(
"auth/process/<slug:factor>/",
view.AuthenticationView.as_view(),
name="auth-process",
),
path("flows/execute/", HttpExecutorView.as_view(), name="flows-execute"),
# User views
path("_/user/", user.UserSettingsView.as_view(), name="user-settings"),
path("_/user/delete/", user.UserDeleteView.as_view(), name="user-delete"),
Expand Down
6 changes: 3 additions & 3 deletions passbook/core/views/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from passbook.core.models import Invitation, Nonce, Source, User
from passbook.core.signals import invitation_used, user_signed_up
from passbook.factors.password.exceptions import PasswordPolicyInvalid
from passbook.factors.view import AuthenticationView, _redirect_with_qs
from passbook.flows.executor.http import bootstrap_http_executor, redirect_with_qs
from passbook.lib.config import CONFIG

LOGGER = get_logger()
Expand Down Expand Up @@ -71,8 +71,8 @@ def form_valid(self, form: LoginForm) -> HttpResponse:
if not pre_user:
# No user found
return self.invalid_login(self.request)
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return _redirect_with_qs("passbook_core:auth-process", self.request.GET)
bootstrap_http_executor(self.request, pre_user)
return redirect_with_qs("passbook_core:flows-execute", self.request.GET)

def invalid_login(
self, request: HttpRequest, disabled_user: User = None
Expand Down
8 changes: 4 additions & 4 deletions passbook/factors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
from django.views.generic import TemplateView

from passbook.core.models import User
from passbook.factors.view import AuthenticationView
from passbook.flows.executor.http import HttpExecutorView
from passbook.lib.config import CONFIG


class AuthenticationFactor(TemplateView):
class Factor(TemplateView):
"""Abstract Authentication factor, inherits TemplateView but can be combined with FormView"""

form: ModelForm = None
required: bool = True
authenticator: AuthenticationView
authenticator: HttpExecutorView
pending_user: User
request: HttpRequest = None
template_name = "login/form_with_user.html"

def __init__(self, authenticator: AuthenticationView):
def __init__(self, authenticator: HttpExecutorView):
self.authenticator = authenticator
self.pending_user = None

Expand Down
4 changes: 2 additions & 2 deletions passbook/factors/dummy/factor.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""passbook multi-factor authentication engine"""
from django.http import HttpRequest

from passbook.factors.base import AuthenticationFactor
from passbook.factors.base import Factor


class DummyFactor(AuthenticationFactor):
class DummyFactor(Factor):
"""Dummy factor for testing with multiple factors"""

def post(self, request: HttpRequest):
Expand Down
2 changes: 1 addition & 1 deletion passbook/factors/forms.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""factor forms"""

GENERAL_FIELDS = ["name", "slug", "order", "policies", "enabled"]
GENERAL_FIELDS = ["name", "slug", "policies"]
1 change: 0 additions & 1 deletion passbook/factors/otp/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ class Meta:
fields = GENERAL_FIELDS + ["enforced"]
widgets = {
"name": forms.TextInput(),
"order": forms.NumberInput(),
"policies": FilteredSelectMultiple(_("policies"), False),
}
help_texts = {
Expand Down
11 changes: 4 additions & 7 deletions passbook/factors/password/factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
from structlog import get_logger

from passbook.core.models import User
from passbook.factors.base import AuthenticationFactor
from passbook.factors.base import Factor
from passbook.factors.password.forms import PasswordForm
from passbook.factors.view import AuthenticationView
from passbook.flows.executor.http import HttpExecutorView
from passbook.lib.config import CONFIG
from passbook.lib.utils.reflection import path_to_class

Expand Down Expand Up @@ -52,7 +52,7 @@ def authenticate(request, backends, **credentials) -> Optional[User]:
)


class PasswordFactor(FormView, AuthenticationFactor):
class PasswordFactor(FormView, Factor):
"""Authentication factor which authenticates against django's AuthBackend"""

form_class = PasswordForm
Expand All @@ -72,10 +72,7 @@ def form_valid(self, form):
)
if user:
# User instance returned from authenticate() has .backend property set
self.authenticator.pending_user = user
self.request.session[
AuthenticationView.SESSION_USER_BACKEND
] = user.backend
self.authenticator.pending_user.backend = user.backend
return self.authenticator.user_ok()
# No user was found -> invalid credentials
LOGGER.debug("Invalid credentials")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ def create_initial_factor(apps, schema_editor):
PasswordFactor.objects.create(
name="password",
slug="password",
order=0,
backends=["django.contrib.auth.backends.ModelBackend"],
)

Expand Down
Loading