flows: Load Stages without refreshing the whole page (#33)
* flows: initial implementation of FlowExecutorShell * flows: load messages dynamically upon card refresh
This commit is contained in:
parent
eeeb14a045
commit
beabba2890
|
@ -5,4 +5,4 @@ load-plugins=pylint_django,pylint.extensions.bad_builtin
|
|||
extension-pkg-whitelist=lxml
|
||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
||||
ignored-modules=django-otp
|
||||
jobs=4
|
||||
jobs=12
|
||||
|
|
|
@ -10,14 +10,14 @@ from passbook.api.permissions import CustomObjectPermissions
|
|||
from passbook.audit.api import EventViewSet
|
||||
from passbook.core.api.applications import ApplicationViewSet
|
||||
from passbook.core.api.groups import GroupViewSet
|
||||
from passbook.core.api.policies import PolicyViewSet
|
||||
from passbook.core.api.messages import MessagesViewSet
|
||||
from passbook.core.api.propertymappings import PropertyMappingViewSet
|
||||
from passbook.core.api.providers import ProviderViewSet
|
||||
from passbook.core.api.sources import SourceViewSet
|
||||
from passbook.core.api.users import UserViewSet
|
||||
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
|
||||
from passbook.lib.utils.reflection import get_apps
|
||||
from passbook.policies.api import PolicyBindingViewSet
|
||||
from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet
|
||||
from passbook.policies.dummy.api import DummyPolicyViewSet
|
||||
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||
from passbook.policies.expression.api import ExpressionPolicyViewSet
|
||||
|
@ -55,6 +55,7 @@ for _passbook_app in get_apps():
|
|||
router.register("core/applications", ApplicationViewSet)
|
||||
router.register("core/groups", GroupViewSet)
|
||||
router.register("core/users", UserViewSet)
|
||||
router.register("core/messages", MessagesViewSet, basename="messages")
|
||||
|
||||
router.register("audit/events", EventViewSet)
|
||||
|
||||
|
|
36
passbook/core/api/messages.py
Normal file
36
passbook/core/api/messages.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
"""core messages API"""
|
||||
from django.contrib.messages import get_messages
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ReadOnlyField, Serializer
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
|
||||
class MessageSerializer(Serializer):
|
||||
"""Serialize Django Message into DRF Object"""
|
||||
|
||||
message = ReadOnlyField()
|
||||
level = ReadOnlyField()
|
||||
tags = ReadOnlyField()
|
||||
extra_tags = ReadOnlyField()
|
||||
level_tag = ReadOnlyField()
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MessagesViewSet(ViewSet):
|
||||
"""Read-only view set that returns the current session's messages"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@swagger_auto_schema(responses={200: MessageSerializer(many=True)})
|
||||
def list(self, request: Request) -> Response:
|
||||
"""List current messages and pass into Serializer"""
|
||||
all_messages = list(get_messages(request))
|
||||
return Response(MessageSerializer(all_messages, many=True).data)
|
|
@ -1,31 +0,0 @@
|
|||
"""Policy API Views"""
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from passbook.policies.forms import GENERAL_FIELDS
|
||||
from passbook.policies.models import Policy
|
||||
|
||||
|
||||
class PolicySerializer(ModelSerializer):
|
||||
"""Policy Serializer"""
|
||||
|
||||
__type__ = SerializerMethodField(method_name="get_type")
|
||||
|
||||
def get_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("policy", "")
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Policy
|
||||
fields = ["pk"] + GENERAL_FIELDS + ["__type__"]
|
||||
|
||||
|
||||
class PolicyViewSet(ReadOnlyModelViewSet):
|
||||
"""Policy Viewset"""
|
||||
|
||||
queryset = Policy.objects.all()
|
||||
serializer_class = PolicySerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return Policy.objects.select_subclasses()
|
|
@ -1,96 +1,22 @@
|
|||
{% extends 'base/skeleton.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<div class="pf-c-background-image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
||||
<filter id="image_overlay">
|
||||
<feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
|
||||
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
|
||||
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
|
||||
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
|
||||
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
|
||||
<feFuncA type="table" tableValues="0 1"></feFuncA>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
{% include 'partials/messages.html' %}
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<header class="pf-c-login__header">
|
||||
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;"
|
||||
alt="passbook icon" />
|
||||
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;"
|
||||
alt="passbook branding" />
|
||||
</header>
|
||||
<main class="pf-c-login__main">
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block card_title %}
|
||||
{% trans title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% include 'partials/form.html' %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">Log in</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
{% if config.login.subtext %}
|
||||
<p>{{ config.login.subtext }}</p>
|
||||
{% endif %}
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
{% for source in sources %}
|
||||
<li class="pf-c-login__main-footer-links-item">
|
||||
<a href="{{ source.url }}" class="pf-c-login__main-footer-links-item-link">
|
||||
{% if source.icon_path %}
|
||||
<img src="{% static source.icon_path %}" alt="{{ source.name }}">
|
||||
{% elif source.icon_url %}
|
||||
<img src="icon_url" alt="{{ source.name }}">
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-arrow" title="{{ source.name }}"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if enroll_url or recovery_url %}
|
||||
<div class="pf-c-login__main-footer-band">
|
||||
{% if enroll_url %}
|
||||
<p class="pf-c-login__main-footer-band-item">
|
||||
{% trans 'Need an account?' %}
|
||||
<a href="{{ enroll_url }}">{% trans 'Sign up.' %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if recovery_url %}
|
||||
<p class="pf-c-login__main-footer-band-item">
|
||||
<a href="{{ recovery_url }}">
|
||||
{% trans 'Forgot username or password?' %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</main>
|
||||
<footer class="pf-c-login__footer">
|
||||
<p></p>
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
<li>
|
||||
<a href="https://beryju.github.io/passbook/">{% trans 'Documentation' %}</a>
|
||||
</li>
|
||||
<!-- todo: load config.passbook.footer.links -->
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block card_title %}
|
||||
{% trans title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% include 'partials/form.html' %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">Log in</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,29 +3,6 @@
|
|||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.form-control-static {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-control-static .left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
line-height: 32px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block above_form %}
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
|
|
|
@ -5,7 +5,7 @@ from random import SystemRandom
|
|||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.core.views.utils import LoadingView, PermissionDeniedView
|
||||
from passbook.core.views.utils import PermissionDeniedView
|
||||
|
||||
|
||||
class TestUtilViews(TestCase):
|
||||
|
@ -22,13 +22,6 @@ class TestUtilViews(TestCase):
|
|||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_loading_view(self):
|
||||
"""Test loading view"""
|
||||
request = self.factory.get("something")
|
||||
response = LoadingView.as_view(target_url="somestring")(request)
|
||||
response.render()
|
||||
self.assertIn("somestring", response.rendered_content)
|
||||
|
||||
def test_permission_denied_view(self):
|
||||
"""Test PermissionDeniedView"""
|
||||
request = self.factory.get("something")
|
||||
|
|
|
@ -3,23 +3,6 @@ from django.utils.translation import ugettext as _
|
|||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class LoadingView(TemplateView):
|
||||
"""View showing a loading template, and forwarding to real view using html forwarding."""
|
||||
|
||||
template_name = "login/loading.html"
|
||||
title = _("Loading")
|
||||
target_url = None
|
||||
|
||||
def get_url(self):
|
||||
"""Return URL template will redirect to"""
|
||||
return self.target_url
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["title"] = self.title
|
||||
kwargs["target_url"] = self.get_url()
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class PermissionDeniedView(TemplateView):
|
||||
"""Generic Permission denied view"""
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ class FlowForm(forms.ModelForm):
|
|||
"slug",
|
||||
"designation",
|
||||
"stages",
|
||||
"policies",
|
||||
]
|
||||
help_texts = {
|
||||
"name": _("Shown as the Title in Flow pages."),
|
||||
|
@ -33,7 +32,6 @@ class FlowForm(forms.ModelForm):
|
|||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"stages": FilteredSelectMultiple(_("stages"), False),
|
||||
"policies": FilteredSelectMultiple(_("policies"), False),
|
||||
}
|
||||
|
||||
|
||||
|
@ -48,9 +46,7 @@ class FlowStageBindingForm(forms.ModelForm):
|
|||
"stage",
|
||||
"re_evaluate_policies",
|
||||
"order",
|
||||
"policies",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"policies": FilteredSelectMultiple(_("policies"), False),
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
|
@ -24,4 +25,5 @@ class StageView(TemplateView):
|
|||
kwargs["title"] = self.executor.flow.name
|
||||
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
|
||||
kwargs["user"] = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||
kwargs["primary_action"] = _("Continue")
|
||||
return super().get_context_data(**kwargs)
|
||||
|
|
169
passbook/flows/templates/flows/shell.html
Normal file
169
passbook/flows/templates/flows/shell.html
Normal file
|
@ -0,0 +1,169 @@
|
|||
{% extends 'base/skeleton.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.pb-loading,
|
||||
.pf-c-login__main >iframe {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.pb-hidden {
|
||||
display: none
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="pf-c-background-image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
||||
<filter id="image_overlay">
|
||||
<feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
|
||||
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
|
||||
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
|
||||
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
|
||||
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
|
||||
<feFuncA type="table" tableValues="0 1"></feFuncA>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
<ul class="pf-c-alert-group pf-m-toast">
|
||||
</ul>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<header class="pf-c-login__header">
|
||||
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;"
|
||||
alt="passbook icon" />
|
||||
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;"
|
||||
alt="passbook branding" />
|
||||
</header>
|
||||
<main class="pf-c-login__main" id="flow-body">
|
||||
<div class="pf-c-login__main-body pb-loading">
|
||||
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="pf-c-login__footer">
|
||||
<p></p>
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
<li>
|
||||
<a href="https://beryju.github.io/passbook/">{% trans 'Documentation' %}</a>
|
||||
</li>
|
||||
<!-- todo: load config.passbook.footer.links -->
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const flowBodyUrl = "{{ exec_url }}";
|
||||
const messagesUrl = "{{ msg_url }}";
|
||||
const flowBody = document.querySelector("#flow-body");
|
||||
const spinner = document.querySelector(".pb-loading");
|
||||
|
||||
const updateMessages = () => {
|
||||
let messageContainer = document.querySelector(".pf-c-alert-group");
|
||||
fetch(messagesUrl).then(response => {
|
||||
messageContainer.innerHTML = "";
|
||||
response.json().then(data => {
|
||||
data.forEach(msg => {
|
||||
let icon = "";
|
||||
switch (msg.level_tag) {
|
||||
case 'error':
|
||||
icon = 'fas fa-exclamation-circle'
|
||||
break;
|
||||
case 'warning':
|
||||
icon = 'fas fa-exclamation-triangle'
|
||||
break;
|
||||
case 'success':
|
||||
icon = 'fas fa-check-circle'
|
||||
break;
|
||||
case 'info':
|
||||
icon = 'fas fa-info'
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (msg.level_tag === "error") {
|
||||
msg.extra_tags = "pf-m-danger";
|
||||
}
|
||||
let item = `<li class="pf-c-alert-group__item">
|
||||
<div class="pf-c-alert pf-m-${msg.level_tag} ${msg.extra_tags}">
|
||||
<div class="pf-c-alert__icon">
|
||||
<i class="${icon}"></i>
|
||||
</div>
|
||||
<h4 class="pf-c-alert__title">
|
||||
${msg.message}
|
||||
</h4>
|
||||
</div>
|
||||
</li>`;
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = item;
|
||||
messageContainer.appendChild(template.content.firstChild);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
const updateCard = (response) => {
|
||||
if (!response.ok) {
|
||||
console.log("well");
|
||||
}
|
||||
if (response.redirected && !response.url.endsWith(flowBodyUrl)) {
|
||||
window.location = response.url;
|
||||
} else {
|
||||
response.text().then(text => {
|
||||
flowBody.innerHTML = text;
|
||||
updateMessages();
|
||||
loadFormCode();
|
||||
setFormSubmitHandlers();
|
||||
});
|
||||
}
|
||||
};
|
||||
const showSpinner = () => {
|
||||
flowBody.innerHTML = "";
|
||||
flowBody.appendChild(spinner);
|
||||
};
|
||||
const loadFormCode = () => {
|
||||
document.querySelectorAll("#flow-body script").forEach(script => {
|
||||
let newScript = document.createElement("script");
|
||||
newScript.src = script.src;
|
||||
document.head.appendChild(newScript);
|
||||
});
|
||||
}
|
||||
const setFormSubmitHandlers = () => {
|
||||
document.querySelectorAll("#flow-body form").forEach(form => {
|
||||
console.log(`Setting action for form ${form}`);
|
||||
// debugger;
|
||||
form.action = flowBodyUrl;
|
||||
console.log(`Adding handler for form ${form}`);
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
let formData = new FormData(form);
|
||||
fetch(flowBodyUrl, {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
}).then((response) => {
|
||||
showSpinner();
|
||||
if (!response.url.endsWith(flowBodyUrl)) {
|
||||
window.location = response.url;
|
||||
} else {
|
||||
updateCard(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
fetch(flowBodyUrl).then(updateCard);
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -18,7 +18,7 @@ class TestHelperView(TestCase):
|
|||
flow = Flow.objects.filter(designation=FlowDesignation.INVALIDATION,).first()
|
||||
response = self.client.get(reverse("passbook_flows:default-invalidation"),)
|
||||
expected_url = reverse(
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||
"passbook_flows:flow-executor-shell", kwargs={"flow_slug": flow.slug}
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, expected_url)
|
||||
|
@ -33,7 +33,7 @@ class TestHelperView(TestCase):
|
|||
|
||||
response = self.client.get(reverse("passbook_flows:default-invalidation"),)
|
||||
expected_url = reverse(
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||
"passbook_flows:flow-executor-shell", kwargs={"flow_slug": flow.slug}
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, expected_url)
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.urls import path
|
|||
|
||||
from passbook.flows.models import FlowDesignation
|
||||
from passbook.flows.views import (
|
||||
FlowExecutorShellView,
|
||||
FlowExecutorView,
|
||||
FlowPermissionDeniedView,
|
||||
ToDefaultFlow,
|
||||
|
@ -40,5 +41,8 @@ urlpatterns = [
|
|||
ToDefaultFlow.as_view(designation=FlowDesignation.PASSWORD_CHANGE),
|
||||
name="default-password-change",
|
||||
),
|
||||
path("<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
|
||||
path("b/<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
|
||||
path(
|
||||
"<slug:flow_slug>/", FlowExecutorShellView.as_view(), name="flow-executor-shell"
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
"""passbook multi-stage authentication engine"""
|
||||
from typing import Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views.generic import View
|
||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.views.generic import TemplateView, View
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.views.utils import PermissionDeniedView
|
||||
|
@ -20,6 +22,7 @@ NEXT_ARG_NAME = "next"
|
|||
SESSION_KEY_PLAN = "passbook_flows_plan"
|
||||
|
||||
|
||||
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
||||
class FlowExecutorView(View):
|
||||
"""Stage 1 Flow executor, passing requests to Stage Views"""
|
||||
|
||||
|
@ -172,5 +175,17 @@ class ToDefaultFlow(View):
|
|||
)
|
||||
del self.request.session[SESSION_KEY_PLAN]
|
||||
return redirect_with_qs(
|
||||
"passbook_flows:flow-executor", request.GET, flow_slug=flow.slug
|
||||
"passbook_flows:flow-executor-shell", request.GET, flow_slug=flow.slug
|
||||
)
|
||||
|
||||
|
||||
class FlowExecutorShellView(TemplateView):
|
||||
"""Executor Shell view, loads a dummy card with a spinner
|
||||
that loads the next stage in the background."""
|
||||
|
||||
template_name = "flows/shell.html"
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
kwargs["exec_url"] = reverse("passbook_flows:flow-executor", kwargs=self.kwargs)
|
||||
kwargs["msg_url"] = reverse("passbook_api:messages-list")
|
||||
return kwargs
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
"""policy API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.policies.forms import GENERAL_FIELDS
|
||||
from passbook.policies.models import Policy, PolicyBinding
|
||||
|
||||
|
||||
class PolicyBindingSerializer(ModelSerializer):
|
||||
|
@ -19,3 +20,28 @@ class PolicyBindingViewSet(ModelViewSet):
|
|||
|
||||
queryset = PolicyBinding.objects.all()
|
||||
serializer_class = PolicyBindingSerializer
|
||||
|
||||
|
||||
class PolicySerializer(ModelSerializer):
|
||||
"""Policy Serializer"""
|
||||
|
||||
__type__ = SerializerMethodField(method_name="get_type")
|
||||
|
||||
def get_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("policy", "")
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Policy
|
||||
fields = ["pk"] + GENERAL_FIELDS + ["__type__"]
|
||||
|
||||
|
||||
class PolicyViewSet(ReadOnlyModelViewSet):
|
||||
"""Policy Viewset"""
|
||||
|
||||
queryset = Policy.objects.all()
|
||||
serializer_class = PolicySerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return Policy.objects.select_subclasses()
|
||||
|
|
|
@ -9,13 +9,8 @@ oauth_urlpatterns = [
|
|||
# Custom OAuth 2 Authorize View
|
||||
path(
|
||||
"authorize/",
|
||||
oauth2.PassbookAuthorizationLoadingView.as_view(),
|
||||
name="oauth2-authorize",
|
||||
),
|
||||
path(
|
||||
"authorize/permission_ok/",
|
||||
oauth2.PassbookAuthorizationView.as_view(),
|
||||
name="oauth2-ok-authorize",
|
||||
name="oauth2-authorize",
|
||||
),
|
||||
path(
|
||||
"authorize/permission_denied/",
|
||||
|
|
|
@ -3,35 +3,21 @@ from typing import Optional
|
|||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.forms import Form
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from oauth2_provider.views.base import AuthorizationView
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.core.models import Application
|
||||
from passbook.core.views.access import AccessMixin
|
||||
from passbook.core.views.utils import LoadingView, PermissionDeniedView
|
||||
from passbook.core.views.utils import PermissionDeniedView
|
||||
from passbook.providers.oauth.models import OAuth2Provider
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class PassbookAuthorizationLoadingView(LoginRequiredMixin, LoadingView):
|
||||
"""Show loading view for permission checks"""
|
||||
|
||||
title = _("Checking permissions...")
|
||||
|
||||
def get_url(self):
|
||||
querystring = urlencode(self.request.GET)
|
||||
return (
|
||||
reverse("passbook_providers_oauth:oauth2-ok-authorize") + "?" + querystring
|
||||
)
|
||||
|
||||
|
||||
class OAuthPermissionDenied(PermissionDeniedView):
|
||||
"""Show permission denied view"""
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ LOGGER = get_logger()
|
|||
class BaseOAuthClient:
|
||||
"""Base OAuth Client"""
|
||||
|
||||
session: Session = None
|
||||
session: Session
|
||||
|
||||
def __init__(self, source, token=""): # nosec
|
||||
self.source = source
|
||||
|
|
|
@ -1 +1,23 @@
|
|||
{% extends 'login/form.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block card_title %}
|
||||
{% trans title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% block above_form %}
|
||||
{% endblock %}
|
||||
|
||||
{% include 'partials/form.html' %}
|
||||
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans primary_action %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -85,27 +85,29 @@ class TestIdentificationStage(TestCase):
|
|||
slug="unique-enrollment-string",
|
||||
designation=FlowDesignation.ENROLLMENT,
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
flow=flow, stage=self.stage, order=0,
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||
),
|
||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(flow.slug, response.rendered_content)
|
||||
self.assertIn(flow.name, response.rendered_content)
|
||||
|
||||
def test_recovery_flow(self):
|
||||
"""Test that recovery flow is linked correctly"""
|
||||
flow = Flow.objects.create(
|
||||
name="enroll-test",
|
||||
name="recovery-test",
|
||||
slug="unique-recovery-string",
|
||||
designation=FlowDesignation.RECOVERY,
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
flow=flow, stage=self.stage, order=0,
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||
),
|
||||
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(flow.slug, response.rendered_content)
|
||||
self.assertIn(flow.name, response.rendered_content)
|
||||
|
|
|
@ -1,9 +1,39 @@
|
|||
{% extends 'login/form_with_user.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block beneath_form %}
|
||||
{% if recovery_flow %}
|
||||
<a href="{% url 'passbook_flows:flow-executor' flow_slug=recovery_flow.slug %}">{% trans 'Forgot password?' %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block card_title %}
|
||||
{% trans title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{% trans "Username" %}</span>
|
||||
</label>
|
||||
<div class="form-control-static">
|
||||
<div class="left">
|
||||
<img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
|
||||
{{ user.username }}
|
||||
</div>
|
||||
<div class="right">
|
||||
<a href="{% url 'passbook_flows:default-authentication' %}">{% trans 'Not you?' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'partials/form.html' %}
|
||||
|
||||
{% if recovery_flow %}
|
||||
<a href="{% url 'passbook_flows:flow-executor' flow_slug=recovery_flow.slug %}">{% trans 'Forgot password?' %}</a>
|
||||
{% endif %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans primary_action %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
|
|
@ -311,3 +311,22 @@ input[data-is-monospace] {
|
|||
visibility: visible
|
||||
}
|
||||
}
|
||||
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-control-static .left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
|
|
@ -56,17 +56,19 @@ document.querySelectorAll("input[name=name]").forEach((input) => {
|
|||
});
|
||||
|
||||
// Hamburger Menu
|
||||
document.querySelector(".pf-c-page__header-brand-toggle>button").addEventListener("click", (e) => {
|
||||
const sidebar = document.querySelector(".pf-c-page__sidebar");
|
||||
if (sidebar.classList.contains("pf-m-expanded")) {
|
||||
// Sidebar already expanded
|
||||
sidebar.classList.remove("pf-m-expanded");
|
||||
sidebar.style.zIndex = 0;
|
||||
} else {
|
||||
// Sidebar not expanded yet
|
||||
sidebar.classList.add("pf-m-expanded");
|
||||
sidebar.style.zIndex = 200;
|
||||
}
|
||||
document.querySelectorAll(".pf-c-page__header-brand-toggle>button").forEach((toggle) => {
|
||||
toggle.addEventListener("click", (e) => {
|
||||
const sidebar = document.querySelector(".pf-c-page__sidebar");
|
||||
if (sidebar.classList.contains("pf-m-expanded")) {
|
||||
// Sidebar already expanded
|
||||
sidebar.classList.remove("pf-m-expanded");
|
||||
sidebar.style.zIndex = 0;
|
||||
} else {
|
||||
// Sidebar not expanded yet
|
||||
sidebar.classList.add("pf-m-expanded");
|
||||
sidebar.style.zIndex = 200;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Collapsable Menus in Sidebar
|
||||
|
|
38
swagger.yaml
38
swagger.yaml
|
@ -341,6 +341,21 @@ paths:
|
|||
required: true
|
||||
type: string
|
||||
format: uuid
|
||||
/core/messages/:
|
||||
get:
|
||||
operationId: core_messages_list
|
||||
description: List current messages and pass into Serializer
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Message'
|
||||
tags:
|
||||
- core
|
||||
parameters: []
|
||||
/core/users/:
|
||||
get:
|
||||
operationId: core_users_list
|
||||
|
@ -4917,6 +4932,29 @@ definitions:
|
|||
attributes:
|
||||
title: Attributes
|
||||
type: object
|
||||
Message:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
title: Message
|
||||
type: string
|
||||
readOnly: true
|
||||
level:
|
||||
title: Level
|
||||
type: string
|
||||
readOnly: true
|
||||
tags:
|
||||
title: Tags
|
||||
type: string
|
||||
readOnly: true
|
||||
extra_tags:
|
||||
title: Extra tags
|
||||
type: string
|
||||
readOnly: true
|
||||
level_tag:
|
||||
title: Level tag
|
||||
type: string
|
||||
readOnly: true
|
||||
User:
|
||||
required:
|
||||
- username
|
||||
|
|
Reference in a new issue