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:
Jens L
2020-06-07 16:35:08 +02:00
committed by GitHub
parent f91e02a0ec
commit 4915205678
81 changed files with 1609 additions and 529 deletions

View File

@ -1,11 +1,11 @@
"""Core OAauth Views"""
from typing import Callable, Optional
from typing import Any, Callable, Dict, Optional
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import ugettext as _
@ -13,7 +13,8 @@ from django.views.generic import RedirectView, View
from structlog import get_logger
from passbook.audit.models import Event, EventAction
from passbook.flows.models import Flow, FlowDesignation
from passbook.core.models import User
from passbook.flows.models import Flow
from passbook.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_SSO,
@ -49,18 +50,18 @@ class OAuthRedirect(OAuthClientMixin, RedirectView):
params = None
# pylint: disable=unused-argument
def get_additional_parameters(self, source):
def get_additional_parameters(self, source: OAuthSource) -> Dict[str, Any]:
"Return additional redirect parameters for this source."
return self.params or {}
def get_callback_url(self, source):
def get_callback_url(self, source: OAuthSource) -> str:
"Return the callback url for this source."
return reverse(
"passbook_sources_oauth:oauth-client-callback",
kwargs={"source_slug": source.slug},
)
def get_redirect_url(self, **kwargs):
def get_redirect_url(self, **kwargs) -> str:
"Build redirect url for a given source."
slug = kwargs.get("source_slug", "")
try:
@ -84,7 +85,7 @@ class OAuthCallback(OAuthClientMixin, View):
source_id = None
source = None
def get(self, request, *_, **kwargs):
def get(self, request: HttpRequest, *_, **kwargs) -> HttpResponse:
"""View Get handler"""
slug = kwargs.get("source_slug", "")
try:
@ -143,38 +144,38 @@ class OAuthCallback(OAuthClientMixin, View):
return self.handle_existing_user(self.source, user, connection, info)
# pylint: disable=unused-argument
def get_callback_url(self, source):
def get_callback_url(self, source: OAuthSource) -> str:
"Return callback url if different than the current url."
return False
return ""
# pylint: disable=unused-argument
def get_error_redirect(self, source, reason):
def get_error_redirect(self, source: OAuthSource, reason: str) -> str:
"Return url to redirect on login failure."
return settings.LOGIN_URL
def get_or_create_user(self, source, access, info):
def get_or_create_user(
self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: Dict[str, Any],
) -> User:
"Create a shell auth.User."
raise NotImplementedError()
# pylint: disable=unused-argument
def get_user_id(self, source, info):
"Return unique identifier from the profile info."
id_key = self.source_id or "id"
result = info
try:
for key in id_key.split("."):
result = result[key]
return result
except KeyError:
return None
def get_user_id(
self, source: UserOAuthSourceConnection, info: Dict[str, Any]
) -> Optional[str]:
"""Return unique identifier from the profile info."""
if "id" in info:
return info["id"]
return None
def handle_login(self, user, source, access):
def handle_login_flow(self, flow: Optional[Flow], user: User) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor"""
user = authenticate(
source=access.source, identifier=access.identifier, request=self.request
)
if not flow:
raise Http404
# We run the Flow planner here so we can pass the Pending user in the context
flow = get_object_or_404(Flow, designation=FlowDesignation.AUTHENTICATION)
planner = FlowPlanner(flow)
plan = planner.plan(
self.request,
@ -186,11 +187,17 @@ class OAuthCallback(OAuthClientMixin, View):
)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"passbook_flows:flow-executor", self.request.GET, flow_slug=flow.slug,
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
)
# pylint: disable=unused-argument
def handle_existing_user(self, source, user, access, info):
def handle_existing_user(
self,
source: OAuthSource,
user: User,
access: UserOAuthSourceConnection,
info: Dict[str, Any],
) -> HttpResponse:
"Login user and redirect."
messages.success(
self.request,
@ -199,15 +206,23 @@ class OAuthCallback(OAuthClientMixin, View):
% {"source": self.source.name}
),
)
return self.handle_login(user, source, access)
user = authenticate(
source=access.source, identifier=access.identifier, request=self.request
)
return self.handle_login_flow(source.authentication_flow, user)
def handle_login_failure(self, source, reason):
def handle_login_failure(self, source: OAuthSource, reason: str) -> HttpResponse:
"Message user and redirect on error."
LOGGER.warning("Authentication Failure", reason=reason)
messages.error(self.request, _("Authentication Failed."))
return redirect(self.get_error_redirect(source, reason))
def handle_new_user(self, source, access, info):
def handle_new_user(
self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: Dict[str, Any],
) -> HttpResponse:
"Create a shell auth.User and redirect."
was_authenticated = False
if self.request.user.is_authenticated:
@ -244,7 +259,7 @@ class OAuthCallback(OAuthClientMixin, View):
% {"source": self.source.name}
),
)
return self.handle_login(user, source, access)
return self.handle_login_flow(source.enrollment_flow, user)
class DisconnectView(LoginRequiredMixin, View):