Compare commits

...

6 Commits

Author SHA1 Message Date
5fb1b8044c new release: 0.8.11-beta 2020-02-25 11:38:50 +01:00
b8daab4377 providers/saml: fix AccessRequiredView.dispatch not being called 2020-02-25 11:38:26 +01:00
c5b91bdae8 providers/saml: fix CannotHandleAssertion Error still being sent to sentry 2020-02-24 19:14:43 +01:00
39a208c55f providers/saml: fix wrong key being used for params 2020-02-24 17:48:03 +01:00
a5bfef9b6b providers/saml: fix leftover data in session, fix IdP initiated login
move can_handle calls to binding endpoints (/login/ and /login/initiate/), so that /login/authorize/ works either way, can clean up the session and audit
2020-02-24 17:34:52 +01:00
f1f4cbef9b lib/sentry: fix SentryIgnoredException not being ignored correctly 2020-02-24 17:01:31 +01:00
9 changed files with 116 additions and 95 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.8.10-beta current_version = 0.8.11-beta
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)

View File

@ -16,11 +16,11 @@ jobs:
- name: Building Docker Image - name: Building Docker Image
run: docker build run: docker build
--no-cache --no-cache
-t beryju/passbook:0.8.10-beta -t beryju/passbook:0.8.11-beta
-t beryju/passbook:latest -t beryju/passbook:latest
-f Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.8.10-beta run: docker push beryju/passbook:0.8.11-beta
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest run: docker push beryju/passbook:latest
build-gatekeeper: build-gatekeeper:
@ -37,11 +37,11 @@ jobs:
cd gatekeeper cd gatekeeper
docker build \ docker build \
--no-cache \ --no-cache \
-t beryju/passbook-gatekeeper:0.8.10-beta \ -t beryju/passbook-gatekeeper:0.8.11-beta \
-t beryju/passbook-gatekeeper:latest \ -t beryju/passbook-gatekeeper:latest \
-f Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-gatekeeper:0.8.10-beta run: docker push beryju/passbook-gatekeeper:0.8.11-beta
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-gatekeeper:latest run: docker push beryju/passbook-gatekeeper:latest
build-static: build-static:
@ -66,11 +66,11 @@ jobs:
run: docker build run: docker build
--no-cache --no-cache
--network=$(docker network ls | grep github | awk '{print $1}') --network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:0.8.10-beta -t beryju/passbook-static:0.8.11-beta
-t beryju/passbook-static:latest -t beryju/passbook-static:latest
-f static.Dockerfile . -f static.Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.8.10-beta run: docker push beryju/passbook-static:0.8.11-beta
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest run: docker push beryju/passbook-static:latest
test-release: test-release:

View File

@ -1,6 +1,6 @@
apiVersion: v1 apiVersion: v1
appVersion: "0.8.10-beta" appVersion: "0.8.11-beta"
description: A Helm chart for passbook. description: A Helm chart for passbook.
name: passbook name: passbook
version: "0.8.10-beta" version: "0.8.11-beta"
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png

View File

@ -2,7 +2,7 @@
# This is a YAML-formatted file. # This is a YAML-formatted file.
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
image: image:
tag: 0.8.10-beta tag: 0.8.11-beta
nameOverride: "" nameOverride: ""

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = "0.8.10-beta" __version__ = "0.8.11-beta"

View File

@ -137,6 +137,7 @@ class Event(UUIDModel):
action=self.action, action=self.action,
context=self.context, context=self.context,
client_ip=self.client_ip, client_ip=self.client_ip,
user=self.user,
) )
return super().save(*args, **kwargs) return super().save(*args, **kwargs)

View File

@ -184,7 +184,7 @@ class Processor:
try: try:
self._extract_saml_request() self._extract_saml_request()
except KeyError: except KeyError:
raise CannotHandleAssertion(f"Couldn't find SAML request in user session:") raise CannotHandleAssertion(f"Couldn't find SAML request in user session")
try: try:
self._decode_and_parse_request() self._decode_and_parse_request()

View File

@ -4,11 +4,8 @@
{% load i18n %} {% load i18n %}
{% block card %} {% block card %}
<form method="POST" class="pf-c-form" action="{{ saml_params.acs_url }}"> <form method="POST" class="pf-c-form">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="ACSUrl" value="{{ saml_params.acs_url }}">
<input type="hidden" name="RelayState" value="{{ saml_params.relay_state }}" />
<input type="hidden" name="SAMLResponse" value="{{ saml_params.saml_response }}" />
<div class="pf-c-form__group"> <div class="pf-c-form__group">
<h3> <h3>
{% blocktrans with provider=provider.application.name %} {% blocktrans with provider=provider.application.name %}

View File

@ -21,12 +21,16 @@ from passbook.core.models import Application, Provider
from passbook.lib.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
from passbook.lib.views import bad_request_message from passbook.lib.views import bad_request_message
from passbook.policies.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
from passbook.providers.saml import exceptions from passbook.providers.saml.exceptions import CannotHandleAssertion
from passbook.providers.saml.models import SAMLProvider from passbook.providers.saml.models import SAMLProvider
from passbook.providers.saml.processors.types import SAMLResponseParams from passbook.providers.saml.processors.types import SAMLResponseParams
LOGGER = get_logger() LOGGER = get_logger()
URL_VALIDATOR = URLValidator(schemes=("http", "https")) URL_VALIDATOR = URLValidator(schemes=("http", "https"))
SESSION_KEY_SAML_REQUEST = "SAMLRequest"
SESSION_KEY_SAML_RESPONSE = "SAMLResponse"
SESSION_KEY_RELAY_STATE = "RelayState"
SESSION_KEY_PARAMS = "SAMLParams"
class AccessRequiredView(AccessMixin, View): class AccessRequiredView(AccessMixin, View):
@ -50,14 +54,18 @@ class AccessRequiredView(AccessMixin, View):
def _has_access(self) -> bool: def _has_access(self) -> bool:
"""Check if user has access to application""" """Check if user has access to application"""
LOGGER.debug(
"_has_access", user=self.request.user, app=self.provider.application
)
policy_engine = PolicyEngine( policy_engine = PolicyEngine(
self.provider.application.policies.all(), self.request.user, self.request self.provider.application.policies.all(), self.request.user, self.request
) )
policy_engine.build() policy_engine.build()
return policy_engine.passing passing = policy_engine.passing
LOGGER.debug(
"saml_has_access",
user=self.request.user,
app=self.provider.application,
passing=passing,
)
return passing
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
if not request.user.is_authenticated: if not request.user.is_authenticated:
@ -75,20 +83,68 @@ class LoginBeginView(AccessRequiredView):
"""Receives a SAML 2.0 AuthnRequest from a Service Provider and """Receives a SAML 2.0 AuthnRequest from a Service Provider and
stores it in the session prior to enforcing login.""" stores it in the session prior to enforcing login."""
@method_decorator(csrf_exempt) def handler(self, source, application: str) -> HttpResponse:
def dispatch(self, request: HttpRequest, application: str) -> HttpResponse: """Handle SAML Request whether its a POST or a Redirect binding"""
if request.method == "POST":
source = request.POST
else:
source = request.GET
# Store these values now, because Django's login cycle won't preserve them. # Store these values now, because Django's login cycle won't preserve them.
try: try:
request.session["SAMLRequest"] = source["SAMLRequest"] self.request.session[SESSION_KEY_SAML_REQUEST] = source[
SESSION_KEY_SAML_REQUEST
]
except (KeyError, MultiValueDictKeyError): except (KeyError, MultiValueDictKeyError):
return bad_request_message(request, "The SAML request payload is missing.") return bad_request_message(
self.request, "The SAML request payload is missing."
)
request.session["RelayState"] = source.get("RelayState", "") self.request.session[SESSION_KEY_RELAY_STATE] = source.get(
SESSION_KEY_RELAY_STATE, ""
)
try:
self.provider.processor.can_handle(self.request)
params = self.provider.processor.generate_response()
self.request.session[SESSION_KEY_PARAMS] = params
except CannotHandleAssertion as exc:
LOGGER.info(exc)
did_you_mean_link = self.request.build_absolute_uri(
reverse(
"passbook_providers_saml:saml-login-initiate",
kwargs={"application": application},
)
)
did_you_mean_message = (
f" Did you mean to go <a href='{did_you_mean_link}'>here</a>?"
)
return bad_request_message(
self.request, mark_safe(str(exc) + did_you_mean_message)
)
return redirect(
reverse(
"passbook_providers_saml:saml-login-authorize",
kwargs={"application": application},
)
)
@method_decorator(csrf_exempt)
def get(self, request: HttpRequest, application: str) -> HttpResponse:
"""Handle REDIRECT bindings"""
return self.handler(request.GET, application)
@method_decorator(csrf_exempt)
def post(self, request: HttpRequest, application: str) -> HttpResponse:
"""Handle POST Bindings"""
return self.handler(request.POST, application)
class InitiateLoginView(AccessRequiredView):
"""IdP-initiated Login"""
def get(self, request: HttpRequest, application: str) -> HttpResponse:
"""Initiates an IdP-initiated link to a simple SP resource/target URL."""
self.provider.processor.is_idp_initiated = True
self.provider.processor.init_deep_link(request)
params = self.provider.processor.generate_response()
request.session[SESSION_KEY_PARAMS] = params
return redirect( return redirect(
reverse( reverse(
"passbook_providers_saml:saml-login-authorize", "passbook_providers_saml:saml-login-authorize",
@ -101,28 +157,6 @@ class AuthorizeView(AccessRequiredView):
"""Ask the user for authorization to continue to the SP. """Ask the user for authorization to continue to the SP.
Presents a SAML 2.0 Assertion for POSTing back to the Service Provider.""" Presents a SAML 2.0 Assertion for POSTing back to the Service Provider."""
def handle_redirect(
self, params: SAMLResponseParams, skipped_authorization: bool
) -> HttpResponse:
"""Handle direct redirect to SP"""
# Log Application Authorization
Event.new(
EventAction.AUTHORIZE_APPLICATION,
authorized_application=self.provider.application,
skipped_authorization=skipped_authorization,
).from_http(self.request)
return render(
self.request,
"saml/idp/autosubmit_form.html",
{
"url": params.acs_url,
"attrs": {
"SAMLResponse": params.saml_response,
"RelayState": params.relay_state,
},
},
)
def get(self, request: HttpRequest, application: str) -> HttpResponse: def get(self, request: HttpRequest, application: str) -> HttpResponse:
"""Handle get request, i.e. render form""" """Handle get request, i.e. render form"""
# User access gets checked in dispatch # User access gets checked in dispatch
@ -134,33 +168,14 @@ class AuthorizeView(AccessRequiredView):
LOGGER.debug("skipping authz", application=self.provider.application) LOGGER.debug("skipping authz", application=self.provider.application)
return self.post(request, application) return self.post(request, application)
self.provider.processor.can_handle(request)
params = self.provider.processor.generate_response()
return render( return render(
request, request,
"saml/idp/login.html", "saml/idp/login.html",
{ {"provider": self.provider, "title": "Authorize Application",},
"saml_params": params,
"provider": self.provider,
"title": "Authorize Application",
},
) )
except exceptions.CannotHandleAssertion as exc: except KeyError:
LOGGER.error(exc) return bad_request_message(request, "Missing SAML Payload")
did_you_mean_link = request.build_absolute_uri(
reverse(
"passbook_providers_saml:saml-login-initiate",
kwargs={"application": application},
)
)
did_you_mean_message = (
f" Did you mean to go <a href='{did_you_mean_link}'>here</a>?"
)
return bad_request_message(
request, mark_safe(str(exc) + did_you_mean_message)
)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def post(self, request: HttpRequest, application: str) -> HttpResponse: def post(self, request: HttpRequest, application: str) -> HttpResponse:
@ -169,9 +184,28 @@ class AuthorizeView(AccessRequiredView):
# we get here when skip_authorization is True, and after the user accepted # we get here when skip_authorization is True, and after the user accepted
# the authorization form # the authorization form
self.provider.processor.can_handle(request) # Log Application Authorization
saml_params = self.provider.processor.generate_response() Event.new(
return self.handle_redirect(saml_params, True) EventAction.AUTHORIZE_APPLICATION,
authorized_application=self.provider.application,
skipped_authorization=self.provider.application.skip_authorization,
).from_http(self.request)
self.request.session.pop(SESSION_KEY_SAML_REQUEST, None)
self.request.session.pop(SESSION_KEY_SAML_RESPONSE, None)
self.request.session.pop(SESSION_KEY_RELAY_STATE, None)
response: SAMLResponseParams = self.request.session.pop(SESSION_KEY_PARAMS)
return render(
self.request,
"saml/idp/autosubmit_form.html",
{
"url": response.acs_url,
"attrs": {
"ACSUrl": response.acs_url,
SESSION_KEY_SAML_RESPONSE: response.saml_response,
SESSION_KEY_RELAY_STATE: response.relay_state,
},
},
)
@method_decorator(csrf_exempt, name="dispatch") @method_decorator(csrf_exempt, name="dispatch")
@ -205,7 +239,9 @@ class SLOLogout(AccessRequiredView):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def post(self, request: HttpRequest, application: str) -> HttpResponse: def post(self, request: HttpRequest, application: str) -> HttpResponse:
"""Perform logout""" """Perform logout"""
request.session["SAMLRequest"] = request.POST["SAMLRequest"] request.session[SESSION_KEY_SAML_REQUEST] = request.POST[
SESSION_KEY_SAML_REQUEST
]
# TODO: Parse SAML LogoutRequest from POST data, similar to login_process(). # TODO: Parse SAML LogoutRequest from POST data, similar to login_process().
# TODO: Modify the base processor to handle logouts? # TODO: Modify the base processor to handle logouts?
# TODO: Combine this with login_process(), since they are so very similar? # TODO: Combine this with login_process(), since they are so very similar?
@ -260,20 +296,7 @@ class DescriptorDownloadView(AccessRequiredView):
) )
else: else:
response = HttpResponse(metadata, content_type="application/xml") response = HttpResponse(metadata, content_type="application/xml")
response["Content-Disposition"] = ( response[
'attachment; filename="' '%s_passbook_meta.xml"' % self.provider.name "Content-Disposition"
) ] = f'attachment; filename="{self.provider.name}_passbook_meta.xml"'
return response return response
class InitiateLoginView(AccessRequiredView):
"""IdP-initiated Login"""
def get(self, request: HttpRequest, application: str) -> HttpResponse:
"""Initiates an IdP-initiated link to a simple SP resource/target URL."""
return redirect(
reverse(
"passbook_providers_saml:saml-login-authorize",
kwargs={"application": application},
)
)