diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 883142150..481221f50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/Pipfile.lock b/Pipfile.lock index 20b2277dc..332684f95 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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": [ diff --git a/passbook/admin/templates/administration/flow/list.html b/passbook/admin/templates/administration/flow/list.html index c013e9348..0956ead3f 100644 --- a/passbook/admin/templates/administration/flow/list.html +++ b/passbook/admin/templates/administration/flow/list.html @@ -61,6 +61,7 @@ {% trans 'Edit' %} {% trans 'Delete' %} + {% trans 'Execute' %} {% endfor %} diff --git a/passbook/admin/templates/administration/overview.html b/passbook/admin/templates/administration/overview.html index bc586ce7c..1c4cd1a4f 100644 --- a/passbook/admin/templates/administration/overview.html +++ b/passbook/admin/templates/administration/overview.html @@ -120,7 +120,17 @@
- {{ version }} + {% if version >= version_latest %} + + {% blocktrans with version=version %} + {{ version }} (Up-to-date!) + {% endblocktrans %} + {% else %} + + {% blocktrans with version=version latest=version_latest %} + {{ version }} ({{ latest }} is available!) + {% endblocktrans %} + {% endif %}
diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index c1a7f824a..af7e41ec9 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -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//update/", flows.FlowUpdateView.as_view(), name="flow-update", ), + path( + "flows//execute/", + flows.FlowDebugExecuteView.as_view(), + name="flow-execute", + ), path( "flows//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 diff --git a/passbook/admin/views/flows.py b/passbook/admin/views/flows.py index 377cf93f4..2ec591c15 100644 --- a/passbook/admin/views/flows.py +++ b/passbook/admin/views/flows.py @@ -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 ): diff --git a/passbook/admin/views/overview.py b/passbook/admin/views/overview.py index 83d0ea009..5c049cf7d 100644 --- a/passbook/admin/views/overview.py +++ b/passbook/admin/views/overview.py @@ -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 diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py index 44916d093..c4b9a9ce8 100644 --- a/passbook/api/v2/urls.py +++ b/passbook/api/v2/urls.py @@ -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) diff --git a/passbook/admin/templates/administration/audit/list.html b/passbook/audit/templates/audit/list.html similarity index 100% rename from passbook/admin/templates/administration/audit/list.html rename to passbook/audit/templates/audit/list.html diff --git a/passbook/audit/urls.py b/passbook/audit/urls.py index f7327b0ac..83be4fe29 100644 --- a/passbook/audit/urls.py +++ b/passbook/audit/urls.py @@ -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"), +] diff --git a/passbook/admin/views/audit.py b/passbook/audit/views.py similarity index 87% rename from passbook/admin/views/audit.py rename to passbook/audit/views.py index 56b274c8f..90599db49 100644 --- a/passbook/admin/views/audit.py +++ b/passbook/audit/views.py @@ -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 diff --git a/passbook/core/templates/base/page.html b/passbook/core/templates/base/page.html index f3fea0fd7..9b918451f 100644 --- a/passbook/core/templates/base/page.html +++ b/passbook/core/templates/base/page.html @@ -32,8 +32,8 @@ {% if user.is_superuser %}
  • {% trans 'Administrate' %}
  • -
  • {% trans 'Monitor' %}
  • +
  • {% trans 'Monitor' %}
  • {% endif %} diff --git a/passbook/core/templates/login/form_with_user.html b/passbook/core/templates/login/form_with_user.html index a7c2461fe..bed72f78c 100644 --- a/passbook/core/templates/login/form_with_user.html +++ b/passbook/core/templates/login/form_with_user.html @@ -14,7 +14,7 @@ {{ user.username }} diff --git a/passbook/flows/templates/flows/error.html b/passbook/flows/templates/flows/error.html new file mode 100644 index 000000000..9f549077c --- /dev/null +++ b/passbook/flows/templates/flows/error.html @@ -0,0 +1,22 @@ +{% load i18n %} + + + + + diff --git a/passbook/flows/urls.py b/passbook/flows/urls.py index ca83335f7..26b0ebb59 100644 --- a/passbook/flows/urls.py +++ b/passbook/flows/urls.py @@ -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//", FlowExecutorView.as_view(), name="flow-executor"), path( "/", FlowExecutorShellView.as_view(), name="flow-executor-shell" diff --git a/passbook/flows/views.py b/passbook/flows/views.py index c78f1aa38..0e6d0fef5 100644 --- a/passbook/flows/views.py +++ b/passbook/flows/views.py @@ -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, ) - stage_response = self.current_stage_view.get(request, *args, **kwargs) - return to_stage_response(request, stage_response) + 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, ) - stage_response = self.current_stage_view.post(request, *args, **kwargs) - return to_stage_response(request, stage_response) + 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: diff --git a/passbook/providers/oidc/templates/oidc_provider/authorize.html b/passbook/providers/oidc/templates/oidc_provider/authorize.html index 9326ee625..aabbb262e 100644 --- a/passbook/providers/oidc/templates/oidc_provider/authorize.html +++ b/passbook/providers/oidc/templates/oidc_provider/authorize.html @@ -38,7 +38,7 @@ {% blocktrans with user=user %} You are logged in as {{ user }}. Not you? {% endblocktrans %} - {% trans 'Logout' %} + {% trans 'Logout' %}

    diff --git a/passbook/stages/password/templates/stages/password/backend.html b/passbook/stages/password/templates/stages/password/backend.html index a2b3db483..74547608d 100644 --- a/passbook/stages/password/templates/stages/password/backend.html +++ b/passbook/stages/password/templates/stages/password/backend.html @@ -21,7 +21,7 @@ {{ user.username }}
    diff --git a/swagger.yaml b/swagger.yaml index 7ac332932..47353de0a 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -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