WIP Use Flows for Sources and Providers (#32)
* core: start migrating to flows for authorisation * sources/oauth: start type-hinting * core: create default user * core: only show user delete button if an unenrollment flow exists * flows: Correctly check initial policies on flow with context * policies: add more verbosity to engine * sources/oauth: migrate to flows * sources/oauth: fix typing errors * flows: add more tests * sources/oauth: start implementing unittests * sources/ldap: add option to disable user sync, move connection init to model * sources/ldap: re-add default PropertyMappings * providers/saml: re-add default PropertyMappings * admin: fix missing stage count * stages/identification: fix sources not being shown * crypto: fix being unable to save with private key * crypto: re-add default self-signed keypair * policies: rewrite cache_key to prevent wrong cache * sources/saml: migrate to flows for auth and enrollment * stages/consent: add new stage * admin: fix PropertyMapping widget not rendering properly * core: provider.authorization_flow is mandatory * flows: add support for "autosubmit" attribute on form * flows: add InMemoryStage for dynamic stages * flows: optionally allow empty flows from FlowPlanner * providers/saml: update to authorization_flow * sources/*: fix flow executor URL * flows: fix pylint error * flows: wrap responses in JSON object to easily handle redirects * flow: dont cache plan's context * providers/oauth: rewrite OAuth2 Provider to use flows * providers/*: update docstrings of models * core: fix forms not passing help_text through safe * flows: fix HttpResponses not being converted to JSON * providers/oidc: rewrite to use flows * flows: fix linting
This commit is contained in:
@ -2,21 +2,31 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from defusedxml import ElementTree
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from signxml import XMLVerifier
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.planner import (
|
||||
PLAN_CONTEXT_PENDING_USER,
|
||||
PLAN_CONTEXT_SSO,
|
||||
FlowPlanner,
|
||||
)
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
from passbook.lib.utils.urls import redirect_with_qs
|
||||
from passbook.providers.saml.utils.encoding import decode_base64_and_inflate
|
||||
from passbook.sources.saml.exceptions import (
|
||||
MissingSAMLResponse,
|
||||
UnsupportedNameIDFormat,
|
||||
)
|
||||
from passbook.sources.saml.models import SAMLSource
|
||||
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
LOGGER = get_logger()
|
||||
if TYPE_CHECKING:
|
||||
from xml.etree.ElementTree import Element # nosec
|
||||
DEFAULT_BACKEND = "django.contrib.auth.backends.ModelBackend"
|
||||
|
||||
|
||||
class Processor:
|
||||
@ -46,7 +56,9 @@ class Processor:
|
||||
def _verify_signed(self):
|
||||
"""Verify SAML Response's Signature"""
|
||||
verifier = XMLVerifier()
|
||||
verifier.verify(self._root_xml, x509_cert=self._source.signing_kp.certificate)
|
||||
verifier.verify(
|
||||
self._root_xml, x509_cert=self._source.signing_kp.certificate_data
|
||||
)
|
||||
|
||||
def _get_email(self) -> Optional[str]:
|
||||
"""
|
||||
@ -69,18 +81,32 @@ class Processor:
|
||||
)
|
||||
return name_id.text
|
||||
|
||||
def get_user(self) -> User:
|
||||
"""
|
||||
Gets info out of the response and locally logs in this user.
|
||||
May create a local user account first.
|
||||
Returns the user object that was created.
|
||||
"""
|
||||
def prepare_flow(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Prepare flow plan depending on whether or not the user exists"""
|
||||
email = self._get_email()
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
user = User.objects.create_user(username=email, email=email)
|
||||
# TODO: Property Mappings
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
return user
|
||||
matching_users = User.objects.filter(email=email)
|
||||
if matching_users.exists():
|
||||
# User exists already, switch to authentication flow
|
||||
flow = self._source.authentication_flow
|
||||
request.session[SESSION_KEY_PLAN] = FlowPlanner(flow).plan(
|
||||
request,
|
||||
{
|
||||
# Data for authentication
|
||||
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: DEFAULT_BACKEND,
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
},
|
||||
)
|
||||
else:
|
||||
flow = self._source.enrollment_flow
|
||||
request.session[SESSION_KEY_PLAN] = FlowPlanner(flow).plan(
|
||||
request,
|
||||
{
|
||||
# Data for enrollment
|
||||
PLAN_CONTEXT_PROMPT: {"username": email, "email": email},
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
},
|
||||
)
|
||||
return redirect_with_qs(
|
||||
"passbook_flows:flow-executor-shell", request.GET, flow_slug=flow.slug,
|
||||
)
|
||||
|
Reference in New Issue
Block a user