providers/samlv2: remove SAMLv2 from master
This commit is contained in:
		| @ -1,11 +0,0 @@ | |||||||
| """passbook saml provider app config""" |  | ||||||
| from django.apps import AppConfig |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class PassbookProviderSAMLv2Config(AppConfig): |  | ||||||
|     """passbook samlv2 provider app config""" |  | ||||||
|  |  | ||||||
|     name = "passbook.providers.samlv2" |  | ||||||
|     label = "passbook_providers_samlv2" |  | ||||||
|     verbose_name = "passbook Providers.SAMLv2" |  | ||||||
|     mountpoint = "application/samlv2/" |  | ||||||
| @ -1,15 +0,0 @@ | |||||||
| """SAML-related constants""" |  | ||||||
| NS_SAML_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" |  | ||||||
| NS_SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" |  | ||||||
| NS_SIGNATURE = "http://www.w3.org/2000/09/xmldsig#" |  | ||||||
|  |  | ||||||
| REQ_KEY_REQUEST = "SAMLRequest" |  | ||||||
| REQ_KEY_SIGNATURE = "Signature" |  | ||||||
|  |  | ||||||
| SESSION_KEY = "passbook_saml_request" |  | ||||||
|  |  | ||||||
| SAML_ATTRIB_ACS_URL = "AssertionConsumerServiceURL" |  | ||||||
| SAML_ATTRIB_DESTINATION = "Destination" |  | ||||||
| SAML_ATTRIB_ID = "ID" |  | ||||||
| SAML_ATTRIB_ISSUE_INSTANT = "IssueInstant" |  | ||||||
| SAML_ATTRIB_PROTOCOL_BINDING = "ProtocolBinding" |  | ||||||
| @ -1,83 +0,0 @@ | |||||||
| """SAML Request Parse/builder""" |  | ||||||
| from typing import TYPE_CHECKING, Optional |  | ||||||
|  |  | ||||||
| from defusedxml import ElementTree |  | ||||||
| from signxml import XMLVerifier |  | ||||||
|  |  | ||||||
| from passbook.crypto.models import CertificateKeyPair |  | ||||||
| from passbook.providers.samlv2.saml.constants import ( |  | ||||||
|     NS_SAML_ASSERTION, |  | ||||||
|     NS_SAML_PROTOCOL, |  | ||||||
|     SAML_ATTRIB_ACS_URL, |  | ||||||
|     SAML_ATTRIB_DESTINATION, |  | ||||||
|     SAML_ATTRIB_ID, |  | ||||||
|     SAML_ATTRIB_ISSUE_INSTANT, |  | ||||||
|     SAML_ATTRIB_PROTOCOL_BINDING, |  | ||||||
| ) |  | ||||||
| from passbook.providers.samlv2.saml.utils import decode_base64_and_inflate |  | ||||||
|  |  | ||||||
| if TYPE_CHECKING: |  | ||||||
|     from xml.etree.ElementTree import Element  # nosec |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # pylint: disable=too-many-instance-attributes |  | ||||||
| class SAMLRequest: |  | ||||||
|     """SAML Request data class, parse raw base64-encoded data, checks signature and more""" |  | ||||||
|  |  | ||||||
|     _root: "Element" |  | ||||||
|  |  | ||||||
|     acs_url: str |  | ||||||
|     destination: str |  | ||||||
|     id: str |  | ||||||
|     issue_instant: str |  | ||||||
|     protocol_binding: str |  | ||||||
|  |  | ||||||
|     issuer: str |  | ||||||
|  |  | ||||||
|     is_signed: bool |  | ||||||
|     _detached_signature: str |  | ||||||
|  |  | ||||||
|     def __init__(self): |  | ||||||
|         self.acs_url = "" |  | ||||||
|         self.destination = "" |  | ||||||
|         # pylint: disable=invalid-name |  | ||||||
|         self.id = "" |  | ||||||
|         self.issue_instant = "" |  | ||||||
|         self.protocol_binding = "" |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def parse(raw: str, detached_signature: Optional[str] = None) -> "SAMLRequest": |  | ||||||
|         """Prase SAML request from raw string, which can be base64 encoded and deflated. |  | ||||||
|         Optionally accepts a detached_signature, as from a REDIRECT request.""" |  | ||||||
|         decoded_xml = decode_base64_and_inflate(raw) |  | ||||||
|         root = ElementTree.fromstring(decoded_xml) |  | ||||||
|         req = SAMLRequest() |  | ||||||
|         req._root = root  # pylint: disable=protected-access |  | ||||||
|         # Verify the root element's tag |  | ||||||
|         _expected_tag = f"{{{NS_SAML_PROTOCOL}}}AuthnRequest" |  | ||||||
|         if root.tag != _expected_tag: |  | ||||||
|             raise ValueError( |  | ||||||
|                 f"Invalid root tag, got '{root.tag}', expected '{_expected_tag}." |  | ||||||
|             ) |  | ||||||
|         req.acs_url = root.attrib[SAML_ATTRIB_ACS_URL] |  | ||||||
|         req.destination = root.attrib[SAML_ATTRIB_DESTINATION] |  | ||||||
|         req.id = root.attrib[SAML_ATTRIB_ID] |  | ||||||
|         req.issue_instant = root.attrib[SAML_ATTRIB_ISSUE_INSTANT] |  | ||||||
|         req.protocol_binding = root.attrib[SAML_ATTRIB_PROTOCOL_BINDING] |  | ||||||
|         req.issuer = root.find(f"{{{NS_SAML_ASSERTION}}}Issuer").text |  | ||||||
|         # Check if this Request is signed |  | ||||||
|         if detached_signature: |  | ||||||
|             # pylint: disable=protected-access |  | ||||||
|             req._detached_signature = detached_signature |  | ||||||
|         return req |  | ||||||
|  |  | ||||||
|     def verify_signature(self, keypair: CertificateKeyPair): |  | ||||||
|         """Verify signature of SAML Request. |  | ||||||
|         Raises `cryptography.exceptions.InvalidSignature` on validaton failure.""" |  | ||||||
|         verifier = XMLVerifier() |  | ||||||
|         if self._detached_signature: |  | ||||||
|             verifier.verify( |  | ||||||
|                 self._detached_signature, x509_cert=keypair.certificate_data |  | ||||||
|             ) |  | ||||||
|         else: |  | ||||||
|             verifier.verify(self._root, x509_cert=keypair.certificate_data) |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| """SAML Provider logic""" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLProvider: |  | ||||||
|     """SAML Provider""" |  | ||||||
| @ -1,24 +0,0 @@ | |||||||
| """Wrappers to de/encode and de/inflate strings""" |  | ||||||
| import base64 |  | ||||||
| import zlib |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def decode_base64_and_inflate(encoded: str, encoding="utf-8") -> str: |  | ||||||
|     """Base64 decode and ZLib decompress b64string""" |  | ||||||
|     decoded_data = base64.b64decode(encoded) |  | ||||||
|     try: |  | ||||||
|         return zlib.decompress(decoded_data, -15).decode(encoding) |  | ||||||
|     except zlib.error: |  | ||||||
|         return decoded_data.decode(encoding) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def deflate_and_base64_encode(inflated: bytes, encoding="utf-8"): |  | ||||||
|     """Base64 and ZLib Compress b64string""" |  | ||||||
|     zlibbed_str = zlib.compress(inflated) |  | ||||||
|     compressed_string = zlibbed_str[2:-4] |  | ||||||
|     return base64.b64encode(compressed_string).decode(encoding) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def nice64(src): |  | ||||||
|     """ Returns src base64-encoded and formatted nicely for our XML. """ |  | ||||||
|     return base64.b64encode(src).decode("utf-8").replace("\n", "") |  | ||||||
| @ -1,35 +0,0 @@ | |||||||
| """passbook samlv2 URLs""" |  | ||||||
| from django.urls import path |  | ||||||
|  |  | ||||||
| from passbook.providers.samlv2.views import authorize, idp_initiated, slo, sso |  | ||||||
|  |  | ||||||
| urlpatterns = [ |  | ||||||
|     path( |  | ||||||
|         "<slug:app_slug>/authorize/", |  | ||||||
|         authorize.AuthorizeView.as_view(), |  | ||||||
|         name="authorize", |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "<slug:app_slug>/sso/redirect/", |  | ||||||
|         sso.SAMLRedirectBindingView.as_view(), |  | ||||||
|         name="sso-redirect", |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "<slug:app_slug>/sso/post/", sso.SAMLPostBindingView.as_view(), name="sso-post", |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "<slug:app_slug>/slo/redirect/", |  | ||||||
|         slo.SAMLRedirectBindingView.as_view(), |  | ||||||
|         name="slo-redirect", |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "<slug:app_slug>/slo/redirect/", |  | ||||||
|         slo.SAMLPostBindingView.as_view(), |  | ||||||
|         name="slo-post", |  | ||||||
|     ), |  | ||||||
|     path( |  | ||||||
|         "<slug:app_slug>/initiate/", |  | ||||||
|         idp_initiated.IDPInitiatedView.as_view(), |  | ||||||
|         name="initiate", |  | ||||||
|     ), |  | ||||||
| ] |  | ||||||
| @ -1,6 +0,0 @@ | |||||||
| """SAML Provider authorization view""" |  | ||||||
| from django.views.generic import FormView |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthorizeView(FormView): |  | ||||||
|     """Authorization view""" |  | ||||||
| @ -1,31 +0,0 @@ | |||||||
| """SAML base views""" |  | ||||||
| from typing import Optional |  | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse |  | ||||||
| from django.shortcuts import get_object_or_404 |  | ||||||
| from django.views import View |  | ||||||
|  |  | ||||||
| from passbook.core.models import Application |  | ||||||
| from passbook.policies.mixins import PolicyAccessMixin |  | ||||||
| from passbook.providers.samlv2.saml.constants import SESSION_KEY |  | ||||||
| from passbook.providers.samlv2.saml.parser import SAMLRequest |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseSAMLView(PolicyAccessMixin, View): |  | ||||||
|     """Base SAML View to resolve app_slug""" |  | ||||||
|  |  | ||||||
|     application: Application |  | ||||||
|  |  | ||||||
|     def setup(self, request: HttpRequest, *args, **kwargs): |  | ||||||
|         View.setup(self, request, *args, **kwargs) |  | ||||||
|         self.application = self.get_application(self.kwargs.get("app_slug")) |  | ||||||
|  |  | ||||||
|     def get_application(self, app_slug: str) -> Optional[Application]: |  | ||||||
|         """Return application or raise 404""" |  | ||||||
|         return get_object_or_404(Application, slug=app_slug) |  | ||||||
|  |  | ||||||
|     def handle_saml_request(self, request: SAMLRequest) -> HttpResponse: |  | ||||||
|         """Handle SAML Request""" |  | ||||||
|         self.request.SESSION[SESSION_KEY] = request |  | ||||||
|         if self.application.skip_authorization: |  | ||||||
|             pass |  | ||||||
| @ -1,6 +0,0 @@ | |||||||
| """IDP-Initiated Views""" |  | ||||||
| from django.views import View |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class IDPInitiatedView(View): |  | ||||||
|     """IDP-initiated Handler""" |  | ||||||
| @ -1,10 +0,0 @@ | |||||||
| """Single Logout Views""" |  | ||||||
| from django.views import View |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLPostBindingView(View): |  | ||||||
|     """Handle SAML POST-type Requests""" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLRedirectBindingView(View): |  | ||||||
|     """Handle SAML Redirect-type Requests""" |  | ||||||
| @ -1,41 +0,0 @@ | |||||||
| """Single Signon Views""" |  | ||||||
| from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest |  | ||||||
|  |  | ||||||
| from passbook.providers.samlv2.saml.constants import REQ_KEY_REQUEST, REQ_KEY_SIGNATURE |  | ||||||
| from passbook.providers.samlv2.saml.parser import SAMLRequest |  | ||||||
| from passbook.providers.samlv2.views.base import BaseSAMLView |  | ||||||
|  |  | ||||||
| # SAML Authentication flow in passbook |  | ||||||
| # - Parse and Verify SAML Request |  | ||||||
| # - Check access to application (this is done after parsing as it might take a few seconds) |  | ||||||
| # - Ask for user authorization (if required from Application) |  | ||||||
| # - Log Access to audit log |  | ||||||
| # - Create response with unique ID to protect against replay |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLPostBindingView(BaseSAMLView): |  | ||||||
|     """Handle SAML POST-type Requests""" |  | ||||||
|  |  | ||||||
|     # pylint: disable=unused-argument |  | ||||||
|     def post(self, request: HttpRequest, app_slug: str) -> HttpResponse: |  | ||||||
|         """Handle POST Requests""" |  | ||||||
|         if REQ_KEY_REQUEST not in request.POST: |  | ||||||
|             return HttpResponseBadRequest() |  | ||||||
|         raw_saml_request = request.POST.get(REQ_KEY_REQUEST) |  | ||||||
|         detached_signature = request.POST.get(REQ_KEY_SIGNATURE, None) |  | ||||||
|         srq = SAMLRequest.parse(raw_saml_request, detached_signature) |  | ||||||
|         return self.handle_saml_request(srq) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLRedirectBindingView(BaseSAMLView): |  | ||||||
|     """Handle SAML Redirect-type Requests""" |  | ||||||
|  |  | ||||||
|     # pylint: disable=unused-argument |  | ||||||
|     def get(self, request: HttpRequest, app_slug: str) -> HttpResponse: |  | ||||||
|         """Handle GET Requests""" |  | ||||||
|         if REQ_KEY_REQUEST not in request.GET: |  | ||||||
|             return HttpResponseBadRequest() |  | ||||||
|         raw_saml_request = request.GET.get(REQ_KEY_REQUEST) |  | ||||||
|         detached_signature = request.GET.get(REQ_KEY_SIGNATURE, None) |  | ||||||
|         srq = SAMLRequest.parse(raw_saml_request, detached_signature) |  | ||||||
|         return self.handle_saml_request(srq) |  | ||||||
| @ -92,7 +92,6 @@ INSTALLED_APPS = [ | |||||||
|     "passbook.providers.oauth.apps.PassbookProviderOAuthConfig", |     "passbook.providers.oauth.apps.PassbookProviderOAuthConfig", | ||||||
|     "passbook.providers.oidc.apps.PassbookProviderOIDCConfig", |     "passbook.providers.oidc.apps.PassbookProviderOIDCConfig", | ||||||
|     "passbook.providers.saml.apps.PassbookProviderSAMLConfig", |     "passbook.providers.saml.apps.PassbookProviderSAMLConfig", | ||||||
|     "passbook.providers.samlv2.apps.PassbookProviderSAMLv2Config", |  | ||||||
|     "passbook.recovery.apps.PassbookRecoveryConfig", |     "passbook.recovery.apps.PassbookRecoveryConfig", | ||||||
|     "passbook.sources.ldap.apps.PassbookSourceLDAPConfig", |     "passbook.sources.ldap.apps.PassbookSourceLDAPConfig", | ||||||
|     "passbook.sources.oauth.apps.PassbookSourceOAuthConfig", |     "passbook.sources.oauth.apps.PassbookSourceOAuthConfig", | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer