Merge branch 'master' into otp-rework

This commit is contained in:
Jens Langhammer 2020-06-30 11:23:09 +02:00
commit 285a69d91f
19 changed files with 305 additions and 37 deletions

View File

@ -52,6 +52,14 @@ jobs:
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Lint with bandit
run: pipenv run bandit -r passbook
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/python@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
pyright:
runs-on: ubuntu-latest
steps:

12
Pipfile.lock generated
View File

@ -46,18 +46,18 @@
},
"boto3": {
"hashes": [
"sha256:2616351c98eec18d20a1d64b33355c86cd855ac96219d1b8428c9bfc590bde53",
"sha256:7daad26a008c91dd7b82fde17d246d1fe6e4b3813426689ef8bac9017a277cfb"
"sha256:77d926c16ab2ab2bfe68811f3bc987e7d79c97f3e2adebf9265c587cdb1fc47b",
"sha256:7f558165eaa608a5d0e05227ee820f4b3cc74533a52c9dde7eb488eba091d50d"
],
"index": "pypi",
"version": "==1.14.12"
"version": "==1.14.13"
},
"botocore": {
"hashes": [
"sha256:45934d880378777cefeca727f369d1f5aebf6b254e9be58e7c77dd0b059338bb",
"sha256:a94e0e2307f1b9fe3a84660842909cd2680b57a9fc9fb0c3a03b0afb2eadbe21"
"sha256:37b65cc48c99b7dd4d5606e56f76ecd88eb7be392ea8a166df734a6b3035301c",
"sha256:5ac9b53a75852fe4282be8741c8136e6948714fef6eff1bd9babb861f8647ba3"
],
"version": "==1.17.12"
"version": "==1.17.13"
},
"celery": {
"hashes": [

View File

@ -61,6 +61,7 @@
<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>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-execute' pk=flow.pk %}?next={{ request.get_full_path }}">{% trans 'Execute' %}</a>
</td>
</tr>
{% endfor %}

View File

@ -120,7 +120,17 @@
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ version }}
{% if version >= version_latest %}
<i class="pf-icon pf-icon-ok"></i>
{% blocktrans with version=version %}
{{ version }} (Up-to-date!)
{% endblocktrans %}
{% else %}
<i class="pf-icon pf-icon-warning-triangle"></i>
{% blocktrans with version=version latest=version_latest %}
{{ version }} ({{ latest }} is available!)
{% endblocktrans %}
{% endif %}
</div>
</div>

View File

@ -3,7 +3,6 @@ from django.urls import path
from passbook.admin.views import (
applications,
audit,
certificate_key_pair,
debug,
flows,
@ -188,6 +187,11 @@ urlpatterns = [
path(
"flows/<uuid:pk>/update/", flows.FlowUpdateView.as_view(), name="flow-update",
),
path(
"flows/<uuid:pk>/execute/",
flows.FlowDebugExecuteView.as_view(),
name="flow-execute",
),
path(
"flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
),
@ -252,8 +256,6 @@ urlpatterns = [
certificate_key_pair.CertificateKeyPairDeleteView.as_view(),
name="certificatekeypair-delete",
),
# Audit Log
path("audit/", audit.EventListView.as_view(), name="audit-log"),
# Groups
path("groups/", groups.GroupListView.as_view(), name="groups"),
# Debug

View File

@ -5,13 +5,17 @@ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpRequest, HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.flows.forms import FlowForm
from passbook.flows.models import Flow
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
from passbook.flows.views import SESSION_KEY_PLAN, FlowPlanner
from passbook.lib.utils.urls import redirect_with_qs
from passbook.lib.views import CreateAssignPermView
@ -46,6 +50,25 @@ class FlowCreateView(
return super().get_context_data(**kwargs)
class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""Debug exectue flow, setting the current user as pending user"""
model = Flow
permission_required = "passbook_flows.view_flow"
# pylint: disable=unused-argument
def get(self, request: HttpRequest, pk: str) -> HttpResponse:
"""Debug exectue flow, setting the current user as pending user"""
flow: Flow = self.get_object()
planner = FlowPlanner(flow)
planner.use_cache = False
plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user,},)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
)
class FlowUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):

View File

@ -1,7 +1,11 @@
"""passbook administration overview"""
from functools import lru_cache
from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
from packaging.version import Version, parse
from requests import RequestException, get
from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin
@ -12,6 +16,19 @@ from passbook.root.celery import CELERY_APP
from passbook.stages.invitation.models import Invitation
@lru_cache
def latest_version() -> Version:
"""Get latest release from GitHub, cached"""
try:
data = get(
"https://api.github.com/repos/beryju/passbook/releases/latest"
).json()
tag_name = data.get("tag_name")
return parse(tag_name.split("/")[1])
except RequestException:
return parse("0.0.0")
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
"""Overview View"""
@ -33,7 +50,8 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs["stage_count"] = len(Stage.objects.all())
kwargs["flow_count"] = len(Flow.objects.all())
kwargs["invitation_count"] = len(Invitation.objects.all())
kwargs["version"] = __version__
kwargs["version"] = parse(__version__)
kwargs["version_latest"] = latest_version()
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs["providers_without_application"] = Provider.objects.filter(
application=None

View File

@ -32,6 +32,7 @@ from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceView
from passbook.sources.oauth.api import OAuthSourceViewSet
from passbook.sources.saml.api import SAMLSourceViewSet
from passbook.stages.captcha.api import CaptchaStageViewSet
from passbook.stages.consent.api import ConsentStageViewSet
from passbook.stages.dummy.api import DummyStageViewSet
from passbook.stages.email.api import EmailStageViewSet
from passbook.stages.identification.api import IdentificationStageViewSet
@ -85,6 +86,7 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register("stages/all", StageViewSet)
router.register("stages/captcha", CaptchaStageViewSet)
router.register("stages/consent", ConsentStageViewSet)
router.register("stages/email", EmailStageViewSet)
router.register("stages/identification", IdentificationStageViewSet)
router.register("stages/invitation", InvitationStageViewSet)

View File

@ -1,2 +1,9 @@
"""passbook audit urls"""
urlpatterns = []
from django.urls import path
from passbook.audit.views import EventListView
urlpatterns = [
# Audit Log
path("audit/", EventListView.as_view(), name="log"),
]

View File

@ -9,7 +9,7 @@ class EventListView(PermissionListMixin, ListView):
"""Show list of all invitations"""
model = Event
template_name = "administration/audit/list.html"
template_name = "audit/list.html"
permission_required = "passbook_audit.view_event"
ordering = "-created"
paginate_by = 20

View File

@ -32,8 +32,8 @@
{% if user.is_superuser %}
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_app 'passbook_admin' %}"
href="{% url 'passbook_admin:overview' %}">{% trans 'Administrate' %}</a></li>
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_admin:audit-log' %}"
href="{% url 'passbook_admin:audit-log' %}">{% trans 'Monitor' %}</a></li>
<li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_audit:log' %}"
href="{% url 'passbook_audit:log' %}">{% trans 'Monitor' %}</a></li>
{% endif %}
</ul>
</nav>

View File

@ -14,7 +14,7 @@
{{ user.username }}
</div>
<div class="right">
<a href="{% url 'passbook_flows:default-authentication' %}">{% trans 'Not you?' %}</a>
<a href="{% url 'passbook_flows:cancel' %}">{% trans 'Not you?' %}</a>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
{% load i18n %}
<style>
.pb-exception {
font-family: monospace;
overflow-x: scroll;
}
</style>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% trans 'Whoops!' %}
</h1>
</header>
<div class="pf-c-login__main-body">
<h3>
{% trans 'Something went wrong! Please try again later.' %}
</h3>
{% if debug %}
<pre class="pb-exception">{{ tb }}</pre>
{% endif %}
</div>

View File

@ -3,6 +3,7 @@ from django.urls import path
from passbook.flows.models import FlowDesignation
from passbook.flows.views import (
CancelView,
FlowExecutorShellView,
FlowExecutorView,
FlowPermissionDeniedView,
@ -36,6 +37,7 @@ urlpatterns = [
ToDefaultFlow.as_view(designation=FlowDesignation.UNRENOLLMENT),
name="default-unenrollment",
),
path("-/cancel/", CancelView.as_view(), name="cancel"),
path("b/<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
path(
"<slug:flow_slug>/", FlowExecutorShellView.as_view(), name="flow-executor-shell"

View File

@ -1,4 +1,5 @@
"""passbook multi-stage authentication engine"""
from traceback import format_tb
from typing import Any, Dict, Optional
from django.http import (
@ -8,7 +9,7 @@ from django.http import (
HttpResponseRedirect,
JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect, reverse
from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views.decorators.clickjacking import xframe_options_sameorigin
@ -106,8 +107,18 @@ class FlowExecutorView(View):
stage=self.current_stage,
flow_slug=self.flow.slug,
)
try:
stage_response = self.current_stage_view.get(request, *args, **kwargs)
return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except
return to_stage_response(
request,
render(
request,
"flows/error.html",
{"error": exc, "tb": "".join(format_tb(exc.__traceback__)),},
),
)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""pass post request to current stage"""
@ -117,8 +128,18 @@ class FlowExecutorView(View):
stage=self.current_stage,
flow_slug=self.flow.slug,
)
try:
stage_response = self.current_stage_view.post(request, *args, **kwargs)
return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except
return to_stage_response(
request,
render(
request,
"flows/error.html",
{"error": exc, "tb": "".join(format_tb(exc.__traceback__)),},
),
)
def _initiate_plan(self) -> FlowPlan:
planner = FlowPlanner(self.flow)
@ -183,6 +204,30 @@ class FlowPermissionDeniedView(PermissionDeniedView):
"""User could not be authenticated"""
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")
self.request.session[SESSION_KEY_GET] = self.request.GET
return kwargs
class CancelView(View):
"""View which canels the currently active plan"""
def get(self, request: HttpRequest) -> HttpResponse:
"""View which canels the currently active plan"""
if SESSION_KEY_PLAN in request.session:
del request.session[SESSION_KEY_PLAN]
LOGGER.debug("Canceled current plan")
return redirect("passbook_core:overview")
class ToDefaultFlow(View):
"""Redirect to default flow matching by designation"""
@ -206,19 +251,6 @@ class ToDefaultFlow(View):
)
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")
self.request.session[SESSION_KEY_GET] = self.request.GET
return kwargs
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
"""Convert normal HttpResponse into JSON Response"""
if isinstance(source, HttpResponseRedirect) or source.status_code == 302:

View File

@ -38,7 +38,7 @@
{% blocktrans with user=user %}
You are logged in as {{ user }}. Not you?
{% endblocktrans %}
<a href="{% url 'passbook_flows:default-invalidation' %}">{% trans 'Logout' %}</a>
<a href="{% url 'passbook_flows:cancel' %}">{% trans 'Logout' %}</a>
</p>
</div>
<div class="pf-c-form__group pf-m-action">

View File

@ -21,7 +21,7 @@
{{ user.username }}
</div>
<div class="right">
<a href="{% url 'passbook_flows:default-authentication' %}">{% trans 'Not you?' %}</a>
<a href="{% url 'passbook_flows:cancel' %}">{% trans 'Not you?' %}</a>
</div>
</div>
</div>

View File

@ -3275,6 +3275,133 @@ paths:
required: true
type: string
format: uuid
/stages/consent/:
get:
operationId: stages_consent_list
description: ConsentStage Viewset
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: limit
in: query
description: Number of results to return per page.
required: false
type: integer
- name: offset
in: query
description: The initial index from which to return the results.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/ConsentStage'
tags:
- stages
post:
operationId: stages_consent_create
description: ConsentStage Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/ConsentStage'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/ConsentStage'
tags:
- stages
parameters: []
/stages/consent/{stage_uuid}/:
get:
operationId: stages_consent_read
description: ConsentStage Viewset
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/ConsentStage'
tags:
- stages
put:
operationId: stages_consent_update
description: ConsentStage Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/ConsentStage'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/ConsentStage'
tags:
- stages
patch:
operationId: stages_consent_partial_update
description: ConsentStage Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/ConsentStage'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/ConsentStage'
tags:
- stages
delete:
operationId: stages_consent_delete
description: ConsentStage Viewset
parameters: []
responses:
'204':
description: ''
tags:
- stages
parameters:
- name: stage_uuid
in: path
description: A UUID string identifying this Consent Stage.
required: true
type: string
format: uuid
/stages/dummy/:
get:
operationId: stages_dummy_list
@ -6052,6 +6179,20 @@ definitions:
description: Private key, acquired from https://www.google.com/recaptcha/intro/v3.html
type: string
minLength: 1
ConsentStage:
required:
- name
type: object
properties:
pk:
title: Stage uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
minLength: 1
DummyStage:
required:
- name