diff --git a/passbook/sources/oauth/types/azure_ad.py b/passbook/sources/oauth/types/azure_ad.py index 936c1708c..d1fd83a11 100644 --- a/passbook/sources/oauth/types/azure_ad.py +++ b/passbook/sources/oauth/types/azure_ad.py @@ -1,10 +1,10 @@ """AzureAD OAuth2 Views""" -import uuid from typing import Any, Dict +from uuid import UUID from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.views.core import OAuthCallback +from passbook.sources.oauth.views.callback import OAuthCallback @MANAGER.source(kind=RequestKind.callback, name="Azure AD") @@ -12,7 +12,7 @@ class AzureADOAuthCallback(OAuthCallback): """AzureAD OAuth2 Callback""" def get_user_id(self, source: OAuthSource, info: Dict[str, Any]) -> str: - return str(uuid.UUID(info.get("objectId")).int) + return str(UUID(info.get("objectId")).int) def get_user_enroll_context( self, diff --git a/passbook/sources/oauth/types/discord.py b/passbook/sources/oauth/types/discord.py index ba5b61ce8..d1e9fb7eb 100644 --- a/passbook/sources/oauth/types/discord.py +++ b/passbook/sources/oauth/types/discord.py @@ -3,7 +3,8 @@ from typing import Any, Dict from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.sources.oauth.views.callback import OAuthCallback +from passbook.sources.oauth.views.redirect import OAuthRedirect @MANAGER.source(kind=RequestKind.redirect, name="Discord") diff --git a/passbook/sources/oauth/types/facebook.py b/passbook/sources/oauth/types/facebook.py index c46557652..77da60db4 100644 --- a/passbook/sources/oauth/types/facebook.py +++ b/passbook/sources/oauth/types/facebook.py @@ -6,7 +6,8 @@ from facebook import GraphAPI from passbook.sources.oauth.clients import OAuth2Client from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.sources.oauth.views.callback import OAuthCallback +from passbook.sources.oauth.views.redirect import OAuthRedirect @MANAGER.source(kind=RequestKind.redirect, name="Facebook") diff --git a/passbook/sources/oauth/types/github.py b/passbook/sources/oauth/types/github.py index 2d7a7181c..e0eb4f081 100644 --- a/passbook/sources/oauth/types/github.py +++ b/passbook/sources/oauth/types/github.py @@ -3,7 +3,7 @@ from typing import Any, Dict from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.views.core import OAuthCallback +from passbook.sources.oauth.views.callback import OAuthCallback @MANAGER.source(kind=RequestKind.callback, name="GitHub") diff --git a/passbook/sources/oauth/types/google.py b/passbook/sources/oauth/types/google.py index 5e79c6819..9aee80ccf 100644 --- a/passbook/sources/oauth/types/google.py +++ b/passbook/sources/oauth/types/google.py @@ -3,7 +3,8 @@ from typing import Any, Dict from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.sources.oauth.views.callback import OAuthCallback +from passbook.sources.oauth.views.redirect import OAuthRedirect @MANAGER.source(kind=RequestKind.redirect, name="Google") diff --git a/passbook/sources/oauth/types/manager.py b/passbook/sources/oauth/types/manager.py index a84a65f37..12f9a5d0e 100644 --- a/passbook/sources/oauth/types/manager.py +++ b/passbook/sources/oauth/types/manager.py @@ -6,7 +6,8 @@ from django.utils.text import slugify from structlog import get_logger from passbook.sources.oauth.models import OAuthSource -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.sources.oauth.views.callback import OAuthCallback +from passbook.sources.oauth.views.redirect import OAuthRedirect LOGGER = get_logger() diff --git a/passbook/sources/oauth/types/oidc.py b/passbook/sources/oauth/types/oidc.py index 99dfe1522..e3f5cfdcc 100644 --- a/passbook/sources/oauth/types/oidc.py +++ b/passbook/sources/oauth/types/oidc.py @@ -3,7 +3,8 @@ from typing import Any, Dict from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.sources.oauth.views.callback import OAuthCallback +from passbook.sources.oauth.views.redirect import OAuthRedirect @MANAGER.source(kind=RequestKind.redirect, name="OpenID Connect") diff --git a/passbook/sources/oauth/types/reddit.py b/passbook/sources/oauth/types/reddit.py index 6ee13376f..d12ad4867 100644 --- a/passbook/sources/oauth/types/reddit.py +++ b/passbook/sources/oauth/types/reddit.py @@ -6,7 +6,8 @@ from requests.auth import HTTPBasicAuth from passbook.sources.oauth.clients import OAuth2Client from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.sources.oauth.views.callback import OAuthCallback +from passbook.sources.oauth.views.redirect import OAuthRedirect @MANAGER.source(kind=RequestKind.redirect, name="reddit") diff --git a/passbook/sources/oauth/types/twitter.py b/passbook/sources/oauth/types/twitter.py index 15934835f..81c0a5e6d 100644 --- a/passbook/sources/oauth/types/twitter.py +++ b/passbook/sources/oauth/types/twitter.py @@ -2,9 +2,9 @@ from typing import Any, Dict from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection +from passbook.sources.oauth.views.callback import OAuthCallback # from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.views.core import OAuthCallback # @MANAGER.source(kind=RequestKind.callback, name="Twitter") diff --git a/passbook/sources/oauth/urls.py b/passbook/sources/oauth/urls.py index c4dde11df..ff203b219 100644 --- a/passbook/sources/oauth/urls.py +++ b/passbook/sources/oauth/urls.py @@ -1,29 +1,30 @@ -"""passbook oauth_client urls""" +"""passbook OAuth source urls""" from django.urls import path from passbook.sources.oauth.types.manager import RequestKind -from passbook.sources.oauth.views import core, dispatcher, user +from passbook.sources.oauth.views.dispatcher import DispatcherView +from passbook.sources.oauth.views.user import DisconnectView, UserSettingsView urlpatterns = [ path( "login//", - dispatcher.DispatcherView.as_view(kind=RequestKind.redirect), + DispatcherView.as_view(kind=RequestKind.redirect), name="oauth-client-login", ), path( "callback//", - dispatcher.DispatcherView.as_view(kind=RequestKind.callback), + DispatcherView.as_view(kind=RequestKind.callback), name="oauth-client-callback", ), - path( - "disconnect//", - core.DisconnectView.as_view(), - name="oauth-client-disconnect", - ), path( "user//", - user.UserSettingsView.as_view(), + UserSettingsView.as_view(), name="oauth-client-user", ), + path( + "user//disconnect/", + DisconnectView.as_view(), + name="oauth-client-disconnect", + ), ] diff --git a/passbook/sources/oauth/views/base.py b/passbook/sources/oauth/views/base.py new file mode 100644 index 000000000..402c489bb --- /dev/null +++ b/passbook/sources/oauth/views/base.py @@ -0,0 +1,19 @@ +"""OAuth Base views""" +from typing import Callable, Optional + +from passbook.sources.oauth.clients import BaseOAuthClient, get_client +from passbook.sources.oauth.models import OAuthSource + + +# pylint: disable=too-few-public-methods +class OAuthClientMixin: + "Mixin for getting OAuth client for a source." + + client_class: Optional[Callable] = None + + def get_client(self, source: OAuthSource) -> BaseOAuthClient: + "Get instance of the OAuth client for this source." + if self.client_class is not None: + # pylint: disable=not-callable + return self.client_class(source) + return get_client(source) diff --git a/passbook/sources/oauth/views/core.py b/passbook/sources/oauth/views/callback.py similarity index 71% rename from passbook/sources/oauth/views/core.py rename to passbook/sources/oauth/views/callback.py index 760c8df56..1f547029c 100644 --- a/passbook/sources/oauth/views/core.py +++ b/passbook/sources/oauth/views/callback.py @@ -1,11 +1,10 @@ -"""Core OAauth Views""" +"""OAuth Callback Views""" from typing import Any, Callable, Dict, Optional from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin from django.http import Http404, HttpRequest, HttpResponse -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import ugettext as _ from django.views.generic import RedirectView, View @@ -25,62 +24,13 @@ from passbook.policies.utils import delete_none_keys from passbook.sources.oauth.auth import AuthorizedServiceBackend from passbook.sources.oauth.clients import BaseOAuthClient, get_client from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection +from passbook.sources.oauth.views.base import OAuthClientMixin from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT LOGGER = get_logger() -# pylint: disable=too-few-public-methods -class OAuthClientMixin: - "Mixin for getting OAuth client for a source." - - client_class: Optional[Callable] = None - - def get_client(self, source: OAuthSource) -> BaseOAuthClient: - "Get instance of the OAuth client for this source." - if self.client_class is not None: - # pylint: disable=not-callable - return self.client_class(source) - return get_client(source) - - -class OAuthRedirect(OAuthClientMixin, RedirectView): - "Redirect user to OAuth source to enable access." - - permanent = False - params = None - - # pylint: disable=unused-argument - 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: 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) -> str: - "Build redirect url for a given source." - slug = kwargs.get("source_slug", "") - try: - source = OAuthSource.objects.get(slug=slug) - except OAuthSource.DoesNotExist: - raise Http404(f"Unknown OAuth source '{slug}'.") - else: - if not source.enabled: - raise Http404(f"source {slug} is not enabled.") - client = self.get_client(source) - callback = self.get_callback_url(source) - params = self.get_additional_parameters(source) - return client.get_redirect_url( - self.request, callback=callback, parameters=params - ) - - class OAuthCallback(OAuthClientMixin, View): "Base OAuth callback view." @@ -258,46 +208,3 @@ class OAuthCallback(OAuthClientMixin, View): ) } return self.handle_login_flow(source.enrollment_flow, **context) - - -class DisconnectView(LoginRequiredMixin, View): - """Delete connection with source""" - - source = None - aas = None - - def dispatch(self, request, source_slug): - self.source = get_object_or_404(OAuthSource, slug=source_slug) - self.aas = get_object_or_404( - UserOAuthSourceConnection, source=self.source, user=request.user - ) - return super().dispatch(request, source_slug) - - def post(self, request, source_slug): - """Delete connection object""" - if "confirmdelete" in request.POST: - # User confirmed deletion - self.aas.delete() - messages.success(request, _("Connection successfully deleted")) - return redirect( - reverse( - "passbook_sources_oauth:oauth-client-user", - kwargs={"source_slug": self.source.slug}, - ) - ) - return self.get(request, source_slug) - - # pylint: disable=unused-argument - def get(self, request, source_slug): - """Show delete form""" - return render( - request, - "generic/delete.html", - { - "object": self.source, - "delete_url": reverse( - "passbook_sources_oauth:oauth-client-disconnect", - kwargs={"source_slug": self.source.slug}, - ), - }, - ) diff --git a/passbook/sources/oauth/views/redirect.py b/passbook/sources/oauth/views/redirect.py new file mode 100644 index 000000000..fc18ce8eb --- /dev/null +++ b/passbook/sources/oauth/views/redirect.py @@ -0,0 +1,67 @@ +"""OAuth Redirect Views""" +from typing import Any, Callable, Dict, Optional + +from django.conf import settings +from django.contrib import messages +from django.http import Http404, HttpRequest, HttpResponse +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.translation import ugettext as _ +from django.views.generic import RedirectView, View +from structlog import get_logger + +from passbook.audit.models import Event, EventAction +from passbook.core.models import User +from passbook.flows.models import Flow +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.policies.utils import delete_none_keys +from passbook.sources.oauth.auth import AuthorizedServiceBackend +from passbook.sources.oauth.clients import BaseOAuthClient, get_client +from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection +from passbook.sources.oauth.views.base import OAuthClientMixin +from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND +from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT + +LOGGER = get_logger() + + +class OAuthRedirect(OAuthClientMixin, RedirectView): + "Redirect user to OAuth source to enable access." + + permanent = False + params = None + + # pylint: disable=unused-argument + 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: 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) -> str: + "Build redirect url for a given source." + slug = kwargs.get("source_slug", "") + try: + source = OAuthSource.objects.get(slug=slug) + except OAuthSource.DoesNotExist: + raise Http404(f"Unknown OAuth source '{slug}'.") + else: + if not source.enabled: + raise Http404(f"source {slug} is not enabled.") + client = self.get_client(source) + callback = self.get_callback_url(source) + params = self.get_additional_parameters(source) + return client.get_redirect_url( + self.request, callback=callback, parameters=params + ) diff --git a/passbook/sources/oauth/views/user.py b/passbook/sources/oauth/views/user.py index f05ab2549..d92d14998 100644 --- a/passbook/sources/oauth/views/user.py +++ b/passbook/sources/oauth/views/user.py @@ -1,7 +1,13 @@ """passbook oauth_client user views""" +from typing import Optional + +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.shortcuts import get_object_or_404 -from django.views.generic import TemplateView +from django.http import HttpRequest, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils.translation import ugettext as _ +from django.views.generic import TemplateView, View from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection @@ -19,3 +25,46 @@ class UserSettingsView(LoginRequiredMixin, TemplateView): kwargs["source"] = source kwargs["connections"] = connections return super().get_context_data(**kwargs) + + +class DisconnectView(LoginRequiredMixin, View): + """Delete connection with source""" + + source: Optional[OAuthSource] = None + aas: Optional[UserOAuthSourceConnection] = None + + def dispatch(self, request: HttpRequest, source_slug: str) -> HttpResponse: + self.source = get_object_or_404(OAuthSource, slug=source_slug) + self.aas = get_object_or_404( + UserOAuthSourceConnection, source=self.source, user=request.user + ) + return super().dispatch(request, source_slug) + + def post(self, request: HttpRequest, source_slug: str) -> HttpResponse: + """Delete connection object""" + if "confirmdelete" in request.POST: + # User confirmed deletion + self.aas.delete() + messages.success(request, _("Connection successfully deleted")) + return redirect( + reverse( + "passbook_sources_oauth:oauth-client-user", + kwargs={"source_slug": self.source.slug}, + ) + ) + return self.get(request, source_slug) + + # pylint: disable=unused-argument + def get(self, request: HttpRequest, source_slug: str) -> HttpResponse: + """Show delete form""" + return render( + request, + "generic/delete.html", + { + "object": self.source, + "delete_url": reverse( + "passbook_sources_oauth:oauth-client-disconnect", + kwargs={"source_slug": self.source.slug}, + ), + }, + )