diff --git a/passbook/admin/forms/inlet.py b/passbook/admin/forms/inlet.py new file mode 100644 index 000000000..71e7f1cfe --- /dev/null +++ b/passbook/admin/forms/inlet.py @@ -0,0 +1,4 @@ +"""passbook core inlet form fields""" + +INLET_FORM_FIELDS = ["name", "slug", "enabled"] +INLET_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"] diff --git a/passbook/admin/forms/source.py b/passbook/admin/forms/source.py deleted file mode 100644 index b18d7157a..000000000 --- a/passbook/admin/forms/source.py +++ /dev/null @@ -1,4 +0,0 @@ -"""passbook core source form fields""" - -SOURCE_FORM_FIELDS = ["name", "slug", "enabled"] -SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"] diff --git a/passbook/admin/templates/administration/source/list.html b/passbook/admin/templates/administration/inlet/list.html similarity index 100% rename from passbook/admin/templates/administration/source/list.html rename to passbook/admin/templates/administration/inlet/list.html diff --git a/passbook/admin/templates/administration/provider/list.html b/passbook/admin/templates/administration/outlet/list.html similarity index 100% rename from passbook/admin/templates/administration/provider/list.html rename to passbook/admin/templates/administration/outlet/list.html diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index 0f3526713..889682789 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -8,12 +8,12 @@ from passbook.admin.views import ( debug, flows, groups, + inlets, invitations, + outlets, overview, - policy, + policies, property_mapping, - providers, - sources, stages, users, ) @@ -39,51 +39,49 @@ urlpatterns = [ applications.ApplicationDeleteView.as_view(), name="application-delete", ), - # Sources - path("sources/", sources.SourceListView.as_view(), name="sources"), - path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"), + # Inlets + path("inlets/", inlets.InletListView.as_view(), name="inlets"), + path("inlets/create/", inlets.InletCreateView.as_view(), name="inlet-create"), path( - "sources//update/", - sources.SourceUpdateView.as_view(), - name="source-update", + "inlets//update/", + inlets.InletUpdateView.as_view(), + name="inlet-update", ), path( - "sources//delete/", - sources.SourceDeleteView.as_view(), - name="source-delete", + "inlets//delete/", + inlets.InletDeleteView.as_view(), + name="inlet-delete", ), # Policies - path("policies/", policy.PolicyListView.as_view(), name="policies"), - path("policies/create/", policy.PolicyCreateView.as_view(), name="policy-create"), + path("policies/", policies.PolicyListView.as_view(), name="policies"), + path("policies/create/", policies.PolicyCreateView.as_view(), name="policy-create"), path( "policies//update/", - policy.PolicyUpdateView.as_view(), + policies.PolicyUpdateView.as_view(), name="policy-update", ), path( "policies//delete/", - policy.PolicyDeleteView.as_view(), + policies.PolicyDeleteView.as_view(), name="policy-delete", ), path( - "policies//test/", policy.PolicyTestView.as_view(), name="policy-test" + "policies//test/", + policies.PolicyTestView.as_view(), + name="policy-test", ), - # Providers - path("providers/", providers.ProviderListView.as_view(), name="providers"), + # Outlets + path("outlets/", outlets.OutletListView.as_view(), name="outlets"), + path("outlets/create/", outlets.OutletCreateView.as_view(), name="outlet-create",), path( - "providers/create/", - providers.ProviderCreateView.as_view(), - name="provider-create", + "outlets//update/", + outlets.OutletUpdateView.as_view(), + name="outlet-update", ), path( - "providers//update/", - providers.ProviderUpdateView.as_view(), - name="provider-update", - ), - path( - "providers//delete/", - providers.ProviderDeleteView.as_view(), - name="provider-delete", + "outlets//delete/", + outlets.OutletDeleteView.as_view(), + name="outlet-delete", ), # Stages path("stages/", stages.StageListView.as_view(), name="stages"), diff --git a/passbook/admin/views/providers.py b/passbook/admin/views/inlets.py similarity index 56% rename from passbook/admin/views/providers.py rename to passbook/admin/views/inlets.py index f0a1d5892..2a277d53e 100644 --- a/passbook/admin/views/providers.py +++ b/passbook/admin/views/inlets.py @@ -1,4 +1,4 @@ -"""passbook Provider administration""" +"""passbook Inlet administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import ( @@ -11,23 +11,23 @@ from django.utils.translation import ugettext as _ from django.views.generic import DeleteView, ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin -from passbook.core.models import Provider +from passbook.core.models import Inlet from passbook.lib.utils.reflection import all_subclasses, path_to_class from passbook.lib.views import CreateAssignPermView -class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView): - """Show list of all providers""" +class InletListView(LoginRequiredMixin, PermissionListMixin, ListView): + """Show list of all inlets""" - model = Provider - permission_required = "passbook_core.add_provider" - template_name = "administration/provider/list.html" - paginate_by = 10 - ordering = "id" + model = Inlet + permission_required = "passbook_core.view_inlet" + ordering = "name" + paginate_by = 40 + template_name = "administration/inlet/list.html" def get_context_data(self, **kwargs): kwargs["types"] = { - x.__name__: x._meta.verbose_name for x in all_subclasses(Provider) + x.__name__: x._meta.verbose_name for x in all_subclasses(Inlet) } return super().get_context_data(**kwargs) @@ -35,40 +35,40 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView): return super().get_queryset().select_subclasses() -class ProviderCreateView( +class InletCreateView( SuccessMessageMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, ): - """Create new Provider""" + """Create new Inlet""" - model = Provider - permission_required = "passbook_core.add_provider" + model = Inlet + permission_required = "passbook_core.add_inlet" template_name = "generic/create.html" - success_url = reverse_lazy("passbook_admin:providers") - success_message = _("Successfully created Provider") + success_url = reverse_lazy("passbook_admin:inlets") + success_message = _("Successfully created Inlet") def get_form_class(self): - provider_type = self.request.GET.get("type") - model = next(x for x in all_subclasses(Provider) if x.__name__ == provider_type) + inlet_type = self.request.GET.get("type") + model = next(x for x in all_subclasses(Inlet) if x.__name__ == inlet_type) if not model: raise Http404 return path_to_class(model.form) -class ProviderUpdateView( +class InletUpdateView( SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView ): - """Update provider""" + """Update inlet""" - model = Provider - permission_required = "passbook_core.change_provider" + model = Inlet + permission_required = "passbook_core.change_inlet" template_name = "generic/update.html" - success_url = reverse_lazy("passbook_admin:providers") - success_message = _("Successfully updated Provider") + success_url = reverse_lazy("passbook_admin:inlets") + success_message = _("Successfully updated Inlet") def get_form_class(self): form_class_path = self.get_object().form @@ -77,29 +77,25 @@ class ProviderUpdateView( def get_object(self, queryset=None): return ( - Provider.objects.filter(pk=self.kwargs.get("pk")) - .select_subclasses() - .first() + Inlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() ) -class ProviderDeleteView( +class InletDeleteView( SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView ): - """Delete provider""" + """Delete inlet""" - model = Provider - permission_required = "passbook_core.delete_provider" + model = Inlet + permission_required = "passbook_core.delete_inlet" template_name = "generic/delete.html" - success_url = reverse_lazy("passbook_admin:providers") - success_message = _("Successfully deleted Provider") + success_url = reverse_lazy("passbook_admin:inlets") + success_message = _("Successfully deleted Inlet") def get_object(self, queryset=None): return ( - Provider.objects.filter(pk=self.kwargs.get("pk")) - .select_subclasses() - .first() + Inlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() ) def delete(self, request, *args, **kwargs): diff --git a/passbook/admin/views/sources.py b/passbook/admin/views/outlets.py similarity index 59% rename from passbook/admin/views/sources.py rename to passbook/admin/views/outlets.py index b5d46af2f..ac77d5f30 100644 --- a/passbook/admin/views/sources.py +++ b/passbook/admin/views/outlets.py @@ -1,4 +1,4 @@ -"""passbook Source administration""" +"""passbook Outlet administration""" from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import ( @@ -11,23 +11,23 @@ from django.utils.translation import ugettext as _ from django.views.generic import DeleteView, ListView, UpdateView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin -from passbook.core.models import Source +from passbook.core.models import Outlet from passbook.lib.utils.reflection import all_subclasses, path_to_class from passbook.lib.views import CreateAssignPermView -class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView): - """Show list of all sources""" +class OutletListView(LoginRequiredMixin, PermissionListMixin, ListView): + """Show list of all outlets""" - model = Source - permission_required = "passbook_core.view_source" - ordering = "name" - paginate_by = 40 - template_name = "administration/source/list.html" + model = Outlet + permission_required = "passbook_core.add_outlet" + template_name = "administration/outlet/list.html" + paginate_by = 10 + ordering = "id" def get_context_data(self, **kwargs): kwargs["types"] = { - x.__name__: x._meta.verbose_name for x in all_subclasses(Source) + x.__name__: x._meta.verbose_name for x in all_subclasses(Outlet) } return super().get_context_data(**kwargs) @@ -35,40 +35,40 @@ class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView): return super().get_queryset().select_subclasses() -class SourceCreateView( +class OutletCreateView( SuccessMessageMixin, LoginRequiredMixin, DjangoPermissionRequiredMixin, CreateAssignPermView, ): - """Create new Source""" + """Create new Outlet""" - model = Source - permission_required = "passbook_core.add_source" + model = Outlet + permission_required = "passbook_core.add_outlet" template_name = "generic/create.html" - success_url = reverse_lazy("passbook_admin:sources") - success_message = _("Successfully created Source") + success_url = reverse_lazy("passbook_admin:outlets") + success_message = _("Successfully created Outlet") def get_form_class(self): - source_type = self.request.GET.get("type") - model = next(x for x in all_subclasses(Source) if x.__name__ == source_type) + outlet_type = self.request.GET.get("type") + model = next(x for x in all_subclasses(Outlet) if x.__name__ == outlet_type) if not model: raise Http404 return path_to_class(model.form) -class SourceUpdateView( +class OutletUpdateView( SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView ): - """Update source""" + """Update outlet""" - model = Source - permission_required = "passbook_core.change_source" + model = Outlet + permission_required = "passbook_core.change_outlet" template_name = "generic/update.html" - success_url = reverse_lazy("passbook_admin:sources") - success_message = _("Successfully updated Source") + success_url = reverse_lazy("passbook_admin:outlets") + success_message = _("Successfully updated Outlet") def get_form_class(self): form_class_path = self.get_object().form @@ -77,25 +77,25 @@ class SourceUpdateView( def get_object(self, queryset=None): return ( - Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() + Outlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() ) -class SourceDeleteView( +class OutletDeleteView( SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView ): - """Delete source""" + """Delete outlet""" - model = Source - permission_required = "passbook_core.delete_source" + model = Outlet + permission_required = "passbook_core.delete_outlet" template_name = "generic/delete.html" - success_url = reverse_lazy("passbook_admin:sources") - success_message = _("Successfully deleted Source") + success_url = reverse_lazy("passbook_admin:outlets") + success_message = _("Successfully deleted Outlet") def get_object(self, queryset=None): return ( - Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() + Outlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first() ) def delete(self, request, *args, **kwargs): diff --git a/passbook/admin/views/overview.py b/passbook/admin/views/overview.py index 1b1b0a88b..cb83aeb59 100644 --- a/passbook/admin/views/overview.py +++ b/passbook/admin/views/overview.py @@ -5,8 +5,9 @@ from django.views.generic import TemplateView from passbook import __version__ from passbook.admin.mixins import AdminRequiredMixin -from passbook.core.models import Application, Policy, Provider, Source, User +from passbook.core.models import Application, Inlet, Outlet, User from passbook.flows.models import Flow, Stage +from passbook.policies.models import Policy from passbook.root.celery import CELERY_APP from passbook.stages.invitation.models import Invitation @@ -27,16 +28,14 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView): kwargs["application_count"] = len(Application.objects.all()) kwargs["policy_count"] = len(Policy.objects.all()) kwargs["user_count"] = len(User.objects.all()) - kwargs["provider_count"] = len(Provider.objects.all()) - kwargs["source_count"] = len(Source.objects.all()) + kwargs["outlet_count"] = len(Outlet.objects.all()) + kwargs["inlet_count"] = len(Inlet.objects.all()) kwargs["stage_count"] = len(Stage.objects.all()) kwargs["flow_count"] = len(Flow.objects.all()) kwargs["invitation_count"] = len(Invitation.objects.all()) kwargs["version"] = __version__ kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5)) - kwargs["providers_without_application"] = Provider.objects.filter( - application=None - ) + kwargs["outlets_without_application"] = Outlet.objects.filter(application=None) kwargs["policies_without_binding"] = len( Policy.objects.filter(policymodel__isnull=True) ) diff --git a/passbook/admin/views/policy.py b/passbook/admin/views/policies.py similarity index 99% rename from passbook/admin/views/policy.py rename to passbook/admin/views/policies.py index 7c65c1d5c..5faa67c5f 100644 --- a/passbook/admin/views/policy.py +++ b/passbook/admin/views/policies.py @@ -13,10 +13,10 @@ from django.views.generic.detail import DetailView from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from passbook.admin.forms.policies import PolicyTestForm -from passbook.core.models import Policy from passbook.lib.utils.reflection import all_subclasses, path_to_class from passbook.lib.views import CreateAssignPermView from passbook.policies.engine import PolicyEngine +from passbook.policies.models import Policy class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView): diff --git a/passbook/admin/views/users.py b/passbook/admin/views/users.py index 618bbf907..6acca6863 100644 --- a/passbook/admin/views/users.py +++ b/passbook/admin/views/users.py @@ -16,7 +16,7 @@ from guardian.mixins import ( ) from passbook.admin.forms.users import UserForm -from passbook.core.models import Nonce, User +from passbook.core.models import Token, User from passbook.lib.views import CreateAssignPermView @@ -92,12 +92,12 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV permission_required = "passbook_core.reset_user_password" def get(self, request, *args, **kwargs): - """Create nonce for user and return link""" + """Create token for user and return link""" super().get(request, *args, **kwargs) # TODO: create plan for user, get token - nonce = Nonce.objects.create(user=self.object) + token = Token.objects.create(user=self.object) link = request.build_absolute_uri( - reverse("passbook_flows:default-recovery", kwargs={"nonce": nonce.uuid}) + reverse("passbook_flows:default-recovery", kwargs={"token": token.uuid}) ) messages.success( request, _("Password reset link:
%(link)s
" % {"link": link}) diff --git a/passbook/api/permissions.py b/passbook/api/permissions.py index 90a7a02af..15fd85d30 100644 --- a/passbook/api/permissions.py +++ b/passbook/api/permissions.py @@ -1,8 +1,8 @@ """permission classes for django restframework""" from rest_framework.permissions import BasePermission, DjangoObjectPermissions -from passbook.core.models import PolicyModel from passbook.policies.engine import PolicyEngine +from passbook.policies.models import PolicyBindingModel class CustomObjectPermissions(DjangoObjectPermissions): @@ -24,8 +24,7 @@ class PolicyPermissions(BasePermission): policy_engine: PolicyEngine - def has_object_permission(self, request, view, obj: PolicyModel) -> bool: - # if not obj.po - self.policy_engine = PolicyEngine(obj.policies, request.user, request) + def has_object_permission(self, request, view, obj: PolicyBindingModel) -> bool: + self.policy_engine = PolicyEngine(obj.policies.all(), request.user, request) self.policy_engine.request.obj = obj return self.policy_engine.build().passing diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py index 9e5c8604c..53d598520 100644 --- a/passbook/api/v2/urls.py +++ b/passbook/api/v2/urls.py @@ -9,12 +9,18 @@ from structlog import get_logger from passbook.api.permissions import CustomObjectPermissions from passbook.audit.api import EventViewSet +from passbook.channels.in_ldap.api import LDAPInletViewSet, LDAPPropertyMappingViewSet +from passbook.channels.in_oauth.api import OAuthInletViewSet +from passbook.channels.out_app_gw.api import ApplicationGatewayOutletViewSet +from passbook.channels.out_oauth.api import OAuth2OutletViewSet +from passbook.channels.out_oidc.api import OpenIDOutletViewSet +from passbook.channels.out_saml.api import SAMLOutletViewSet, SAMLPropertyMappingViewSet from passbook.core.api.applications import ApplicationViewSet from passbook.core.api.groups import GroupViewSet +from passbook.core.api.inlets import InletViewSet +from passbook.core.api.outlets import OutletViewSet from passbook.core.api.policies import PolicyViewSet from passbook.core.api.propertymappings import PropertyMappingViewSet -from passbook.core.api.providers import ProviderViewSet -from passbook.core.api.sources import SourceViewSet from passbook.core.api.users import UserViewSet from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet from passbook.lib.utils.reflection import get_apps @@ -24,12 +30,6 @@ from passbook.policies.expression.api import ExpressionPolicyViewSet from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet from passbook.policies.password.api import PasswordPolicyViewSet from passbook.policies.reputation.api import ReputationPolicyViewSet -from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet -from passbook.providers.oauth.api import OAuth2ProviderViewSet -from passbook.providers.oidc.api import OpenIDProviderViewSet -from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet -from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet -from passbook.sources.oauth.api import OAuthSourceViewSet from passbook.stages.captcha.api import CaptchaStageViewSet from passbook.stages.email.api import EmailStageViewSet from passbook.stages.identification.api import IdentificationStageViewSet @@ -57,9 +57,15 @@ router.register("core/users", UserViewSet) router.register("audit/events", EventViewSet) -router.register("sources/all", SourceViewSet) -router.register("sources/ldap", LDAPSourceViewSet) -router.register("sources/oauth", OAuthSourceViewSet) +router.register("inlets/all", InletViewSet) +router.register("inlets/ldap", LDAPInletViewSet) +router.register("inlets/oauth", OAuthInletViewSet) + +router.register("outlets/all", OutletViewSet) +router.register("outlets/applicationgateway", ApplicationGatewayOutletViewSet) +router.register("outlets/oauth", OAuth2OutletViewSet) +router.register("outlets/openid", OpenIDOutletViewSet) +router.register("outlets/saml", SAMLOutletViewSet) router.register("policies/all", PolicyViewSet) router.register("policies/bindings", PolicyBindingViewSet) @@ -69,12 +75,6 @@ router.register("policies/password", PasswordPolicyViewSet) router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet) router.register("policies/reputation", ReputationPolicyViewSet) -router.register("providers/all", ProviderViewSet) -router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet) -router.register("providers/oauth", OAuth2ProviderViewSet) -router.register("providers/openid", OpenIDProviderViewSet) -router.register("providers/saml", SAMLProviderViewSet) - router.register("propertymappings/all", PropertyMappingViewSet) router.register("propertymappings/ldap", LDAPPropertyMappingViewSet) router.register("propertymappings/saml", SAMLPropertyMappingViewSet) diff --git a/passbook/audit/migrations/0001_initial.py b/passbook/audit/migrations/0001_initial.py index 77ade9f0c..63bb2fc56 100644 --- a/passbook/audit/migrations/0001_initial.py +++ b/passbook/audit/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.6 on 2019-10-07 14:07 +# Generated by Django 3.0.5 on 2020-05-15 19:58 import uuid @@ -18,7 +18,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="AuditEntry", + name="Event", fields=[ ( "uuid", @@ -33,15 +33,16 @@ class Migration(migrations.Migration): "action", models.TextField( choices=[ - ("login", "login"), - ("login_failed", "login_failed"), - ("logout", "logout"), - ("authorize_application", "authorize_application"), - ("suspicious_request", "suspicious_request"), - ("sign_up", "sign_up"), - ("password_reset", "password_reset"), - ("invitation_created", "invitation_created"), - ("invitation_used", "invitation_used"), + ("LOGIN", "login"), + ("LOGIN_FAILED", "login_failed"), + ("LOGOUT", "logout"), + ("AUTHORIZE_APPLICATION", "authorize_application"), + ("SUSPICIOUS_REQUEST", "suspicious_request"), + ("SIGN_UP", "sign_up"), + ("PASSWORD_RESET", "password_reset"), + ("INVITE_CREATED", "invitation_created"), + ("INVITE_USED", "invitation_used"), + ("CUSTOM", "custom"), ] ), ), @@ -53,7 +54,7 @@ class Migration(migrations.Migration): blank=True, default=dict ), ), - ("request_ip", models.GenericIPAddressField()), + ("client_ip", models.GenericIPAddressField(null=True)), ("created", models.DateTimeField(auto_now_add=True)), ( "user", @@ -65,8 +66,8 @@ class Migration(migrations.Migration): ), ], options={ - "verbose_name": "Audit Entry", - "verbose_name_plural": "Audit Entries", + "verbose_name": "Audit Event", + "verbose_name_plural": "Audit Events", }, ), ] diff --git a/passbook/audit/migrations/0002_auto_20191028_0829.py b/passbook/audit/migrations/0002_auto_20191028_0829.py deleted file mode 100644 index 9a582528c..000000000 --- a/passbook/audit/migrations/0002_auto_20191028_0829.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2.6 on 2019-10-28 08:29 - -from django.conf import settings -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("passbook_audit", "0001_initial"), - ] - - operations = [ - migrations.RenameModel(old_name="AuditEntry", new_name="Event",), - ] diff --git a/passbook/audit/migrations/0003_auto_20191205_1407.py b/passbook/audit/migrations/0003_auto_20191205_1407.py deleted file mode 100644 index 61c7a4e77..000000000 --- a/passbook/audit/migrations/0003_auto_20191205_1407.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 2.2.8 on 2019-12-05 14:07 - -from django.db import migrations, models - -import passbook.audit.models - - -class Migration(migrations.Migration): - - dependencies = [ - ("passbook_audit", "0002_auto_20191028_0829"), - ] - - operations = [ - migrations.AlterModelOptions( - name="event", - options={ - "verbose_name": "Audit Event", - "verbose_name_plural": "Audit Events", - }, - ), - migrations.AlterField( - model_name="event", - name="action", - field=models.TextField( - choices=[ - ("LOGIN", "login"), - ("LOGIN_FAILED", "login_failed"), - ("LOGOUT", "logout"), - ("AUTHORIZE_APPLICATION", "authorize_application"), - ("SUSPICIOUS_REQUEST", "suspicious_request"), - ("SIGN_UP", "sign_up"), - ("PASSWORD_RESET", "password_reset"), - ("INVITE_CREATED", "invitation_created"), - ("INVITE_USED", "invitation_used"), - ("CUSTOM", "custom"), - ] - ), - ), - ] diff --git a/passbook/audit/migrations/0004_auto_20191205_1502.py b/passbook/audit/migrations/0004_auto_20191205_1502.py deleted file mode 100644 index bd45599eb..000000000 --- a/passbook/audit/migrations/0004_auto_20191205_1502.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.8 on 2019-12-05 15:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("passbook_audit", "0003_auto_20191205_1407"), - ] - - operations = [ - migrations.RemoveField(model_name="event", name="request_ip",), - migrations.AddField( - model_name="event", - name="client_ip", - field=models.GenericIPAddressField(null=True), - ), - ] diff --git a/passbook/audit/tests/test_event.py b/passbook/audit/tests/test_event.py index 5e32b2d09..9d7e189f5 100644 --- a/passbook/audit/tests/test_event.py +++ b/passbook/audit/tests/test_event.py @@ -5,7 +5,7 @@ from django.test import TestCase from guardian.shortcuts import get_anonymous_user from passbook.audit.models import Event, EventAction -from passbook.core.models import Policy +from passbook.policies.models import Policy class TestAuditEvent(TestCase): diff --git a/passbook/providers/__init__.py b/passbook/channels/__init__.py similarity index 100% rename from passbook/providers/__init__.py rename to passbook/channels/__init__.py diff --git a/passbook/providers/app_gw/__init__.py b/passbook/channels/in_ldap/__init__.py similarity index 100% rename from passbook/providers/app_gw/__init__.py rename to passbook/channels/in_ldap/__init__.py diff --git a/passbook/sources/ldap/api.py b/passbook/channels/in_ldap/api.py similarity index 69% rename from passbook/sources/ldap/api.py rename to passbook/channels/in_ldap/api.py index a51a5ce12..433fe57a5 100644 --- a/passbook/sources/ldap/api.py +++ b/passbook/channels/in_ldap/api.py @@ -1,17 +1,17 @@ -"""Source API Views""" +"""Inlet API Views""" from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet -from passbook.admin.forms.source import SOURCE_SERIALIZER_FIELDS -from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource +from passbook.admin.forms.inlet import INLET_SERIALIZER_FIELDS +from passbook.channels.in_ldap.models import LDAPInlet, LDAPPropertyMapping -class LDAPSourceSerializer(ModelSerializer): - """LDAP Source Serializer""" +class LDAPInletSerializer(ModelSerializer): + """LDAP Inlet Serializer""" class Meta: - model = LDAPSource - fields = SOURCE_SERIALIZER_FIELDS + [ + model = LDAPInlet + fields = INLET_SERIALIZER_FIELDS + [ "server_uri", "bind_cn", "bind_password", @@ -38,11 +38,11 @@ class LDAPPropertyMappingSerializer(ModelSerializer): fields = ["pk", "name", "expression", "object_field"] -class LDAPSourceViewSet(ModelViewSet): - """LDAP Source Viewset""" +class LDAPInletViewSet(ModelViewSet): + """LDAP Inlet Viewset""" - queryset = LDAPSource.objects.all() - serializer_class = LDAPSourceSerializer + queryset = LDAPInlet.objects.all() + serializer_class = LDAPInletSerializer class LDAPPropertyMappingViewSet(ModelViewSet): diff --git a/passbook/channels/in_ldap/apps.py b/passbook/channels/in_ldap/apps.py new file mode 100644 index 000000000..d49b75197 --- /dev/null +++ b/passbook/channels/in_ldap/apps.py @@ -0,0 +1,11 @@ +"""Passbook ldap app config""" + +from django.apps import AppConfig + + +class PassbookInletLDAPConfig(AppConfig): + """Passbook ldap app config""" + + name = "passbook.channels.in_ldap" + label = "passbook_channels_in_ldap" + verbose_name = "passbook Inlets.LDAP" diff --git a/passbook/sources/ldap/auth.py b/passbook/channels/in_ldap/auth.py similarity index 67% rename from passbook/sources/ldap/auth.py rename to passbook/channels/in_ldap/auth.py index 390a43b84..4392ad49c 100644 --- a/passbook/sources/ldap/auth.py +++ b/passbook/channels/in_ldap/auth.py @@ -3,8 +3,8 @@ from django.contrib.auth.backends import ModelBackend from django.http import HttpRequest from structlog import get_logger -from passbook.sources.ldap.connector import Connector -from passbook.sources.ldap.models import LDAPSource +from passbook.channels.in_ldap.connector import Connector +from passbook.channels.in_ldap.models import LDAPInlet LOGGER = get_logger() @@ -16,9 +16,9 @@ class LDAPBackend(ModelBackend): """Try to authenticate a user via ldap""" if "password" not in kwargs: return None - for source in LDAPSource.objects.filter(enabled=True): - LOGGER.debug("LDAP Auth attempt", source=source) - _ldap = Connector(source) + for inlet in LDAPInlet.objects.filter(enabled=True): + LOGGER.debug("LDAP Auth attempt", inlet=inlet) + _ldap = Connector(inlet) user = _ldap.auth_user(**kwargs) if user: return user diff --git a/passbook/sources/ldap/connector.py b/passbook/channels/in_ldap/connector.py similarity index 84% rename from passbook/sources/ldap/connector.py rename to passbook/channels/in_ldap/connector.py index 064a6a628..72a2bbb4a 100644 --- a/passbook/sources/ldap/connector.py +++ b/passbook/channels/in_ldap/connector.py @@ -6,9 +6,9 @@ import ldap3.core.exceptions from django.db.utils import IntegrityError from structlog import get_logger +from passbook.channels.in_ldap.models import LDAPInlet, LDAPPropertyMapping from passbook.core.exceptions import PropertyMappingExpressionException from passbook.core.models import Group, User -from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource LOGGER = get_logger() @@ -18,23 +18,23 @@ class Connector: _server: ldap3.Server _connection = ldap3.Connection - _source: LDAPSource + _inlet: LDAPInlet - def __init__(self, source: LDAPSource): - self._source = source + def __init__(self, source: LDAPInlet): + self._inlet = source self._server = ldap3.Server(source.server_uri) # Implement URI parsing def bind(self): - """Bind using Source's Credentials""" + """Bind using Inlet's Credentials""" self._connection = ldap3.Connection( self._server, raise_exceptions=True, - user=self._source.bind_cn, - password=self._source.bind_password, + user=self._inlet.bind_cn, + password=self._inlet.bind_password, ) self._connection.bind() - if self._source.start_tls: + if self._inlet.start_tls: self._connection.start_tls() @staticmethod @@ -45,21 +45,21 @@ class Connector: @property def base_dn_users(self) -> str: """Shortcut to get full base_dn for user lookups""" - return ",".join([self._source.additional_user_dn, self._source.base_dn]) + return ",".join([self._inlet.additional_user_dn, self._inlet.base_dn]) @property def base_dn_groups(self) -> str: """Shortcut to get full base_dn for group lookups""" - return ",".join([self._source.additional_group_dn, self._source.base_dn]) + return ",".join([self._inlet.additional_group_dn, self._inlet.base_dn]) def sync_groups(self): """Iterate over all LDAP Groups and create passbook_core.Group instances""" - if not self._source.sync_groups: - LOGGER.debug("Group syncing is disabled for this Source") + if not self._inlet.sync_groups: + LOGGER.debug("Group syncing is disabled for this Inlet") return groups = self._connection.extend.standard.paged_search( search_base=self.base_dn_groups, - search_filter=self._source.group_object_filter, + search_filter=self._inlet.group_object_filter, search_scope=ldap3.SUBTREE, attributes=ldap3.ALL_ATTRIBUTES, ) @@ -67,15 +67,15 @@ class Connector: attributes = group.get("attributes", {}) _, created = Group.objects.update_or_create( attributes__ldap_uniq=attributes.get( - self._source.object_uniqueness_field, "" + self._inlet.object_uniqueness_field, "" ), - parent=self._source.sync_parent_group, + parent=self._inlet.sync_parent_group, # defaults=self._build_object_properties(attributes), defaults={ "name": attributes.get("name", ""), "attributes": { "ldap_uniq": attributes.get( - self._source.object_uniqueness_field, "" + self._inlet.object_uniqueness_field, "" ), "distinguishedName": attributes.get("distinguishedName"), }, @@ -89,14 +89,14 @@ class Connector: """Iterate over all LDAP Users and create passbook_core.User instances""" users = self._connection.extend.standard.paged_search( search_base=self.base_dn_users, - search_filter=self._source.user_object_filter, + search_filter=self._inlet.user_object_filter, search_scope=ldap3.SUBTREE, attributes=ldap3.ALL_ATTRIBUTES, ) for user in users: attributes = user.get("attributes", {}) try: - uniq = attributes[self._source.object_uniqueness_field] + uniq = attributes[self._inlet.object_uniqueness_field] except KeyError: LOGGER.warning("Cannot find uniqueness Field in attributes") continue @@ -125,20 +125,20 @@ class Connector: """Iterate over all Users and assign Groups using memberOf Field""" users = self._connection.extend.standard.paged_search( search_base=self.base_dn_users, - search_filter=self._source.user_object_filter, + search_filter=self._inlet.user_object_filter, search_scope=ldap3.SUBTREE, attributes=[ - self._source.user_group_membership_field, - self._source.object_uniqueness_field, + self._inlet.user_group_membership_field, + self._inlet.object_uniqueness_field, ], ) group_cache: Dict[str, Group] = {} for user in users: member_of = user.get("attributes", {}).get( - self._source.user_group_membership_field, [] + self._inlet.user_group_membership_field, [] ) uniq = user.get("attributes", {}).get( - self._source.object_uniqueness_field, [] + self._inlet.object_uniqueness_field, [] ) for group_dn in member_of: # Check if group_dn is within our base_dn_groups, and skip if not @@ -168,7 +168,7 @@ class Connector: self, attributes: Dict[str, Any] ) -> Dict[str, Dict[Any, Any]]: properties = {"attributes": {}} - for mapping in self._source.property_mappings.all().select_subclasses(): + for mapping in self._inlet.property_mappings.all().select_subclasses(): if not isinstance(mapping, LDAPPropertyMapping): continue mapping: LDAPPropertyMapping @@ -179,9 +179,9 @@ class Connector: except PropertyMappingExpressionException as exc: LOGGER.warning("Mapping failed to evaluate", exc=exc, mapping=mapping) continue - if self._source.object_uniqueness_field in attributes: + if self._inlet.object_uniqueness_field in attributes: properties["attributes"]["ldap_uniq"] = attributes.get( - self._source.object_uniqueness_field + self._inlet.object_uniqueness_field ) properties["attributes"]["distinguishedName"] = attributes.get( "distinguishedName" diff --git a/passbook/sources/ldap/forms.py b/passbook/channels/in_ldap/forms.py similarity index 86% rename from passbook/sources/ldap/forms.py rename to passbook/channels/in_ldap/forms.py index 249ebd5af..afad81cbf 100644 --- a/passbook/sources/ldap/forms.py +++ b/passbook/channels/in_ldap/forms.py @@ -4,17 +4,17 @@ from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext_lazy as _ -from passbook.admin.forms.source import SOURCE_FORM_FIELDS -from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource +from passbook.admin.forms.inlet import INLET_FORM_FIELDS +from passbook.channels.in_ldap.models import LDAPInlet, LDAPPropertyMapping -class LDAPSourceForm(forms.ModelForm): - """LDAPSource Form""" +class LDAPInletForm(forms.ModelForm): + """LDAPInlet Form""" class Meta: - model = LDAPSource - fields = SOURCE_FORM_FIELDS + [ + model = LDAPInlet + fields = INLET_FORM_FIELDS + [ "server_uri", "bind_cn", "bind_password", diff --git a/passbook/sources/ldap/migrations/0001_initial.py b/passbook/channels/in_ldap/migrations/0001_initial.py similarity index 53% rename from passbook/sources/ldap/migrations/0001_initial.py rename to passbook/channels/in_ldap/migrations/0001_initial.py index f73d4018d..1e7bba6e7 100644 --- a/passbook/sources/ldap/migrations/0001_initial.py +++ b/passbook/channels/in_ldap/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.6 on 2019-10-08 20:43 +# Generated by Django 3.0.5 on 2020-05-15 19:59 import django.core.validators import django.db.models.deletion @@ -10,7 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("passbook_core", "0001_initial"), + ("passbook_core", "__first__"), ] operations = [ @@ -28,69 +28,104 @@ class Migration(migrations.Migration): to="passbook_core.PropertyMapping", ), ), - ("ldap_property", models.TextField()), ("object_field", models.TextField()), ], - options={"abstract": False,}, + options={ + "verbose_name": "LDAP Property Mapping", + "verbose_name_plural": "LDAP Property Mappings", + }, bases=("passbook_core.propertymapping",), ), migrations.CreateModel( - name="LDAPSource", + name="LDAPInlet", fields=[ ( - "source_ptr", + "inlet_ptr", models.OneToOneField( auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, - to="passbook_core.Source", + to="passbook_core.Inlet", ), ), ( "server_uri", - models.URLField( + models.TextField( validators=[ django.core.validators.URLValidator( schemes=["ldap", "ldaps"] ) - ] + ], + verbose_name="Server URI", ), ), - ("bind_cn", models.TextField()), + ("bind_cn", models.TextField(verbose_name="Bind CN")), ("bind_password", models.TextField()), - ("start_tls", models.BooleanField(default=False)), - ("base_dn", models.TextField()), + ( + "start_tls", + models.BooleanField(default=False, verbose_name="Enable Start TLS"), + ), + ("base_dn", models.TextField(verbose_name="Base DN")), ( "additional_user_dn", models.TextField( - help_text="Prepended to Base DN for User-queries." + help_text="Prepended to Base DN for User-queries.", + verbose_name="Addition User DN", ), ), ( "additional_group_dn", models.TextField( - help_text="Prepended to Base DN for Group-queries." + help_text="Prepended to Base DN for Group-queries.", + verbose_name="Addition Group DN", + ), + ), + ( + "user_object_filter", + models.TextField( + default="(objectCategory=Person)", + help_text="Consider Objects matching this filter to be Users.", + ), + ), + ( + "user_group_membership_field", + models.TextField( + default="memberOf", + help_text="Field which contains Groups of user.", + ), + ), + ( + "group_object_filter", + models.TextField( + default="(objectCategory=Group)", + help_text="Consider Objects matching this filter to be Groups.", + ), + ), + ( + "object_uniqueness_field", + models.TextField( + default="objectSid", + help_text="Field which contains a unique Identifier.", ), ), - ("user_object_filter", models.TextField()), - ("group_object_filter", models.TextField()), ("sync_groups", models.BooleanField(default=True)), ( "sync_parent_group", models.ForeignKey( blank=True, default=None, + null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to="passbook_core.Group", ), ), ], options={ - "verbose_name": "LDAP Source", - "verbose_name_plural": "LDAP Sources", + "verbose_name": "LDAP Inlet", + "verbose_name_plural": "LDAP Inlets", }, - bases=("passbook_core.source",), + bases=("passbook_core.inlet",), ), ] diff --git a/passbook/providers/app_gw/migrations/__init__.py b/passbook/channels/in_ldap/migrations/__init__.py similarity index 100% rename from passbook/providers/app_gw/migrations/__init__.py rename to passbook/channels/in_ldap/migrations/__init__.py diff --git a/passbook/sources/ldap/models.py b/passbook/channels/in_ldap/models.py similarity index 85% rename from passbook/sources/ldap/models.py rename to passbook/channels/in_ldap/models.py index 393cccfa2..68bb8c2f7 100644 --- a/passbook/sources/ldap/models.py +++ b/passbook/channels/in_ldap/models.py @@ -4,11 +4,11 @@ from django.core.validators import URLValidator from django.db import models from django.utils.translation import gettext_lazy as _ -from passbook.core.models import Group, PropertyMapping, Source +from passbook.core.models import Group, Inlet, PropertyMapping -class LDAPSource(Source): - """LDAP Authentication source""" +class LDAPInlet(Inlet): + """LDAP Authentication inlet""" server_uri = models.TextField( validators=[URLValidator(schemes=["ldap", "ldaps"])], @@ -48,12 +48,12 @@ class LDAPSource(Source): Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT ) - form = "passbook.sources.ldap.forms.LDAPSourceForm" + form = "passbook.channels.in_ldap.forms.LDAPInletForm" class Meta: - verbose_name = _("LDAP Source") - verbose_name_plural = _("LDAP Sources") + verbose_name = _("LDAP Inlet") + verbose_name_plural = _("LDAP Inlets") class LDAPPropertyMapping(PropertyMapping): @@ -61,7 +61,7 @@ class LDAPPropertyMapping(PropertyMapping): object_field = models.TextField() - form = "passbook.sources.ldap.forms.LDAPPropertyMappingForm" + form = "passbook.channels.in_ldap.forms.LDAPPropertyMappingForm" def __str__(self): return f"LDAP Property Mapping {self.expression} -> {self.object_field}" diff --git a/passbook/sources/ldap/settings.py b/passbook/channels/in_ldap/settings.py similarity index 64% rename from passbook/sources/ldap/settings.py rename to passbook/channels/in_ldap/settings.py index 392332f1d..a22065e24 100644 --- a/passbook/sources/ldap/settings.py +++ b/passbook/channels/in_ldap/settings.py @@ -2,12 +2,12 @@ from celery.schedules import crontab AUTHENTICATION_BACKENDS = [ - "passbook.sources.ldap.auth.LDAPBackend", + "passbook.channels.in_ldap.auth.LDAPBackend", ] CELERY_BEAT_SCHEDULE = { "sync": { - "task": "passbook.sources.ldap.tasks.sync", + "task": "passbook.channels.in_ldap.tasks.sync", "schedule": crontab(minute=0), # Run every hour } } diff --git a/passbook/channels/in_ldap/tasks.py b/passbook/channels/in_ldap/tasks.py new file mode 100644 index 000000000..c22889113 --- /dev/null +++ b/passbook/channels/in_ldap/tasks.py @@ -0,0 +1,33 @@ +"""LDAP Sync tasks""" +from passbook.channels.in_ldap.connector import Connector +from passbook.channels.in_ldap.models import LDAPInlet +from passbook.root.celery import CELERY_APP + + +@CELERY_APP.task() +def sync_groups(inlet_pk: int): + """Sync LDAP Groups on background worker""" + inlet = LDAPInlet.objects.get(pk=inlet_pk) + connector = Connector(inlet) + connector.bind() + connector.sync_groups() + + +@CELERY_APP.task() +def sync_users(inlet_pk: int): + """Sync LDAP Users on background worker""" + inlet = LDAPInlet.objects.get(pk=inlet_pk) + connector = Connector(inlet) + connector.bind() + connector.sync_users() + + +@CELERY_APP.task() +def sync(): + """Sync all inlets""" + for inlet in LDAPInlet.objects.filter(enabled=True): + connector = Connector(inlet) + connector.bind() + connector.sync_users() + connector.sync_groups() + connector.sync_membership() diff --git a/passbook/sources/ldap/templates/ldap/property_mapping_form.html b/passbook/channels/in_ldap/templates/ldap/property_mapping_form.html similarity index 100% rename from passbook/sources/ldap/templates/ldap/property_mapping_form.html rename to passbook/channels/in_ldap/templates/ldap/property_mapping_form.html diff --git a/passbook/providers/app_gw/provider/__init__.py b/passbook/channels/in_oauth/__init__.py similarity index 100% rename from passbook/providers/app_gw/provider/__init__.py rename to passbook/channels/in_oauth/__init__.py diff --git a/passbook/channels/in_oauth/api.py b/passbook/channels/in_oauth/api.py new file mode 100644 index 000000000..b4c5a6360 --- /dev/null +++ b/passbook/channels/in_oauth/api.py @@ -0,0 +1,29 @@ +"""OAuth Inlet Serializer""" +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from passbook.admin.forms.inlet import INLET_SERIALIZER_FIELDS +from passbook.channels.in_oauth.models import OAuthInlet + + +class OAuthInletSerializer(ModelSerializer): + """OAuth Inlet Serializer""" + + class Meta: + model = OAuthInlet + fields = INLET_SERIALIZER_FIELDS + [ + "inlet_type", + "request_token_url", + "authorization_url", + "access_token_url", + "profile_url", + "consumer_key", + "consumer_secret", + ] + + +class OAuthInletViewSet(ModelViewSet): + """Inlet Viewset""" + + queryset = OAuthInlet.objects.all() + serializer_class = OAuthInletSerializer diff --git a/passbook/sources/oauth/apps.py b/passbook/channels/in_oauth/apps.py similarity index 76% rename from passbook/sources/oauth/apps.py rename to passbook/channels/in_oauth/apps.py index 360cefcec..e579992e5 100644 --- a/passbook/sources/oauth/apps.py +++ b/passbook/channels/in_oauth/apps.py @@ -8,12 +8,12 @@ from structlog import get_logger LOGGER = get_logger() -class PassbookSourceOAuthConfig(AppConfig): +class PassbookInletOAuthConfig(AppConfig): """passbook source.oauth config""" - name = "passbook.sources.oauth" - label = "passbook_sources_oauth" - verbose_name = "passbook Sources.OAuth" + name = "passbook.channels.in_oauth" + label = "passbook_channels_in_oauth" + verbose_name = "passbook Inlets.OAuth" mountpoint = "source/oauth/" def ready(self): diff --git a/passbook/channels/in_oauth/backends.py b/passbook/channels/in_oauth/backends.py new file mode 100644 index 000000000..6a93b9865 --- /dev/null +++ b/passbook/channels/in_oauth/backends.py @@ -0,0 +1,24 @@ +"""passbook oauth_client Authorization backend""" + +from django.contrib.auth.backends import ModelBackend +from django.db.models import Q + +from passbook.channels.in_oauth.models import OAuthInlet, UserOAuthInletConnection + + +class AuthorizedServiceBackend(ModelBackend): + "Authentication backend for users registered with remote OAuth provider." + + def authenticate(self, request, inlet=None, identifier=None): + "Fetch user for a given inlet by id." + inlet_q = Q(inlet__name=inlet) + if isinstance(inlet, OAuthInlet): + inlet_q = Q(inlet=inlet) + try: + access = UserOAuthInletConnection.objects.filter( + inlet_q, identifier=identifier + ).select_related("user")[0] + except IndexError: + return None + else: + return access.user diff --git a/passbook/sources/oauth/clients.py b/passbook/channels/in_oauth/clients.py similarity index 89% rename from passbook/sources/oauth/clients.py rename to passbook/channels/in_oauth/clients.py index f47380a9a..8909e72a3 100644 --- a/passbook/sources/oauth/clients.py +++ b/passbook/channels/in_oauth/clients.py @@ -21,8 +21,8 @@ class BaseOAuthClient: session: Session = None - def __init__(self, source, token=""): # nosec - self.source = source + def __init__(self, inlet, token=""): # nosec + self.inlet = inlet self.token = token self.session = Session() self.session.headers.update({"User-Agent": "passbook %s" % __version__}) @@ -38,7 +38,7 @@ class BaseOAuthClient: "Authorization": f"{token['token_type']} {token['access_token']}" } response = self.session.request( - "get", self.source.profile_url, headers=headers, + "get", self.inlet.profile_url, headers=headers, ) response.raise_for_status() except RequestException as exc: @@ -58,7 +58,7 @@ class BaseOAuthClient: args.update(additional) params = urlencode(args) LOGGER.info("redirect args", **args) - return "{0}?{1}".format(self.source.authorization_url, params) + return "{0}?{1}".format(self.inlet.authorization_url, params) def parse_raw_token(self, raw_token): "Parse token and secret from raw token response." @@ -94,7 +94,7 @@ class OAuthClient(BaseOAuthClient): try: response = self.session.request( "post", - self.source.access_token_url, + self.inlet.access_token_url, data=data, headers=self._default_headers, ) @@ -112,7 +112,7 @@ class OAuthClient(BaseOAuthClient): try: response = self.session.request( "post", - self.source.request_token_url, + self.inlet.request_token_url, data={"oauth_callback": callback}, headers=self._default_headers, ) @@ -151,10 +151,10 @@ class OAuthClient(BaseOAuthClient): callback = kwargs.pop("oauth_callback", None) verifier = kwargs.get("data", {}).pop("oauth_verifier", None) oauth = OAuth1( - resource_owner_key=token, - resource_owner_secret=secret, - client_key=self.source.consumer_key, - client_secret=self.source.consumer_secret, + reinlet_owner_key=token, + reinlet_owner_secret=secret, + client_key=self.inlet.consumer_key, + client_secret=self.inlet.consumer_secret, verifier=verifier, callback_uri=callback, ) @@ -163,7 +163,7 @@ class OAuthClient(BaseOAuthClient): @property def session_key(self): - return "oauth-client-{0}-request-token".format(self.source.name) + return "oauth-client-{0}-request-token".format(self.inlet.name) class OAuth2Client(BaseOAuthClient): @@ -183,7 +183,7 @@ class OAuth2Client(BaseOAuthClient): if returned is not None: check = constant_time_compare(stored, returned) else: - LOGGER.warning("No state parameter returned by the source.") + LOGGER.warning("No state parameter returned by the inlet.") else: LOGGER.warning("No state stored in the sesssion.") return check @@ -196,19 +196,19 @@ class OAuth2Client(BaseOAuthClient): return None if "code" in request.GET: args = { - "client_id": self.source.consumer_key, + "client_id": self.inlet.consumer_key, "redirect_uri": callback, - "client_secret": self.source.consumer_secret, + "client_secret": self.inlet.consumer_secret, "code": request.GET["code"], "grant_type": "authorization_code", } else: - LOGGER.warning("No code returned by the source") + LOGGER.warning("No code returned by the inlet") return None try: response = self.session.request( "post", - self.source.access_token_url, + self.inlet.access_token_url, data=args, headers=self._default_headers, **request_kwargs, @@ -229,7 +229,7 @@ class OAuth2Client(BaseOAuthClient): "Get request parameters for redirect url." callback = request.build_absolute_uri(callback) args = { - "client_id": self.source.consumer_key, + "client_id": self.inlet.consumer_key, "redirect_uri": callback, "response_type": "code", } @@ -264,12 +264,12 @@ class OAuth2Client(BaseOAuthClient): @property def session_key(self): - return "oauth-client-{0}-request-state".format(self.source.name) + return "oauth-client-{0}-request-state".format(self.inlet.name) -def get_client(source, token=""): # nosec - "Return the API client for the given source." +def get_client(inlet, token=""): # nosec + "Return the API client for the given inlet." cls = OAuth2Client - if source.request_token_url: + if inlet.request_token_url: cls = OAuthClient - return cls(source, token) + return cls(inlet, token) diff --git a/passbook/sources/oauth/forms.py b/passbook/channels/in_oauth/forms.py similarity index 72% rename from passbook/sources/oauth/forms.py rename to passbook/channels/in_oauth/forms.py index e1e0de903..27f572833 100644 --- a/passbook/sources/oauth/forms.py +++ b/passbook/channels/in_oauth/forms.py @@ -2,13 +2,13 @@ from django import forms -from passbook.admin.forms.source import SOURCE_FORM_FIELDS -from passbook.sources.oauth.models import OAuthSource -from passbook.sources.oauth.types.manager import MANAGER +from passbook.admin.forms.inlet import INLET_FORM_FIELDS +from passbook.channels.in_oauth.models import OAuthInlet +from passbook.channels.in_oauth.types.manager import MANAGER -class OAuthSourceForm(forms.ModelForm): - """OAuthSource Form""" +class OAuthInletForm(forms.ModelForm): + """OAuthInlet Form""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -19,8 +19,8 @@ class OAuthSourceForm(forms.ModelForm): class Meta: - model = OAuthSource - fields = SOURCE_FORM_FIELDS + [ + model = OAuthInlet + fields = INLET_FORM_FIELDS + [ "provider_type", "request_token_url", "authorization_url", @@ -37,10 +37,10 @@ class OAuthSourceForm(forms.ModelForm): } -class GitHubOAuthSourceForm(OAuthSourceForm): - """OAuth Source form with pre-determined URL for GitHub""" +class GitHubOAuthInletForm(OAuthInletForm): + """OAuth Inlet form with pre-determined URL for GitHub""" - class Meta(OAuthSourceForm.Meta): + class Meta(OAuthInletForm.Meta): overrides = { "provider_type": "github", @@ -51,10 +51,10 @@ class GitHubOAuthSourceForm(OAuthSourceForm): } -class TwitterOAuthSourceForm(OAuthSourceForm): - """OAuth Source form with pre-determined URL for Twitter""" +class TwitterOAuthInletForm(OAuthInletForm): + """OAuth Inlet form with pre-determined URL for Twitter""" - class Meta(OAuthSourceForm.Meta): + class Meta(OAuthInletForm.Meta): overrides = { "provider_type": "twitter", @@ -68,10 +68,10 @@ class TwitterOAuthSourceForm(OAuthSourceForm): } -class FacebookOAuthSourceForm(OAuthSourceForm): - """OAuth Source form with pre-determined URL for Facebook""" +class FacebookOAuthInletForm(OAuthInletForm): + """OAuth Inlet form with pre-determined URL for Facebook""" - class Meta(OAuthSourceForm.Meta): + class Meta(OAuthInletForm.Meta): overrides = { "provider_type": "facebook", @@ -82,10 +82,10 @@ class FacebookOAuthSourceForm(OAuthSourceForm): } -class DiscordOAuthSourceForm(OAuthSourceForm): - """OAuth Source form with pre-determined URL for Discord""" +class DiscordOAuthInletForm(OAuthInletForm): + """OAuth Inlet form with pre-determined URL for Discord""" - class Meta(OAuthSourceForm.Meta): + class Meta(OAuthInletForm.Meta): overrides = { "provider_type": "discord", @@ -96,10 +96,10 @@ class DiscordOAuthSourceForm(OAuthSourceForm): } -class GoogleOAuthSourceForm(OAuthSourceForm): - """OAuth Source form with pre-determined URL for Google""" +class GoogleOAuthInletForm(OAuthInletForm): + """OAuth Inlet form with pre-determined URL for Google""" - class Meta(OAuthSourceForm.Meta): + class Meta(OAuthInletForm.Meta): overrides = { "provider_type": "google", @@ -110,10 +110,10 @@ class GoogleOAuthSourceForm(OAuthSourceForm): } -class AzureADOAuthSourceForm(OAuthSourceForm): - """OAuth Source form with pre-determined URL for AzureAD""" +class AzureADOAuthInletForm(OAuthInletForm): + """OAuth Inlet form with pre-determined URL for AzureAD""" - class Meta(OAuthSourceForm.Meta): + class Meta(OAuthInletForm.Meta): overrides = { "provider_type": "azure-ad", diff --git a/passbook/channels/in_oauth/migrations/0001_initial.py b/passbook/channels/in_oauth/migrations/0001_initial.py new file mode 100644 index 000000000..dbe8b5f24 --- /dev/null +++ b/passbook/channels/in_oauth/migrations/0001_initial.py @@ -0,0 +1,81 @@ +# Generated by Django 3.0.5 on 2020-05-15 19:59 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("passbook_core", "__first__"), + ] + + operations = [ + migrations.CreateModel( + name="OAuthInlet", + fields=[ + ( + "inlet_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Inlet", + ), + ), + ("inlet_type", models.CharField(max_length=255)), + ( + "request_token_url", + models.CharField( + blank=True, max_length=255, verbose_name="Request Token URL" + ), + ), + ( + "authorization_url", + models.CharField(max_length=255, verbose_name="Authorization URL"), + ), + ( + "access_token_url", + models.CharField(max_length=255, verbose_name="Access Token URL"), + ), + ( + "profile_url", + models.CharField(max_length=255, verbose_name="Profile URL"), + ), + ("consumer_key", models.TextField()), + ("consumer_secret", models.TextField()), + ], + options={ + "verbose_name": "Generic OAuth Inlet", + "verbose_name_plural": "Generic OAuth Inlets", + }, + bases=("passbook_core.inlet",), + ), + migrations.CreateModel( + name="UserOAuthInletConnection", + fields=[ + ( + "userinletconnection_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.UserInletConnection", + ), + ), + ("identifier", models.CharField(max_length=255)), + ("access_token", models.TextField(blank=True, default=None, null=True)), + ], + options={ + "verbose_name": "User OAuth Inlet Connection", + "verbose_name_plural": "User OAuth Inlet Connections", + }, + bases=("passbook_core.userinletconnection",), + ), + ] diff --git a/passbook/providers/app_gw/provider/kubernetes/__init__.py b/passbook/channels/in_oauth/migrations/__init__.py similarity index 100% rename from passbook/providers/app_gw/provider/kubernetes/__init__.py rename to passbook/channels/in_oauth/migrations/__init__.py diff --git a/passbook/channels/in_oauth/models.py b/passbook/channels/in_oauth/models.py new file mode 100644 index 000000000..e17ec653f --- /dev/null +++ b/passbook/channels/in_oauth/models.py @@ -0,0 +1,159 @@ +"""OAuth Client models""" + +from django.db import models +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ + +from passbook.channels.in_oauth.clients import get_client +from passbook.core.models import Inlet, UserInletConnection +from passbook.core.types import UILoginButton, UIUserSettings + + +class OAuthInlet(Inlet): + """Configuration for OAuth inlet.""" + + inlet_type = models.CharField(max_length=255) + request_token_url = models.CharField( + blank=True, max_length=255, verbose_name=_("Request Token URL") + ) + authorization_url = models.CharField( + max_length=255, verbose_name=_("Authorization URL") + ) + access_token_url = models.CharField( + max_length=255, verbose_name=_("Access Token URL") + ) + profile_url = models.CharField(max_length=255, verbose_name=_("Profile URL")) + consumer_key = models.TextField() + consumer_secret = models.TextField() + + form = "passbook.channels.in_oauth.forms.OAuthInletForm" + + @property + def ui_login_button(self) -> UILoginButton: + return UILoginButton( + url=reverse_lazy( + "passbook_channels_in_oauth:oauth-client-login", + kwargs={"inlet_slug": self.slug}, + ), + icon_path=f"passbook/inlets/{self.inlet_type}.svg", + name=self.name, + ) + + @property + def ui_additional_info(self) -> str: + url = reverse_lazy( + "passbook_channels_in_oauth:oauth-client-callback", + kwargs={"inlet_slug": self.slug}, + ) + return f"Callback URL:
{url}
" + + @property + def ui_user_settings(self) -> UIUserSettings: + icon_type = self.inlet_type + if icon_type == "azure ad": + icon_type = "windows" + icon_class = f"fab fa-{icon_type}" + view_name = "passbook_channels_in_oauth:oauth-client-user" + return UIUserSettings( + name=self.name, + icon=icon_class, + view_name=reverse((view_name), kwargs={"inlet_slug": self.slug}), + ) + + class Meta: + + verbose_name = _("Generic OAuth Inlet") + verbose_name_plural = _("Generic OAuth Inlets") + + +class GitHubOAuthInlet(OAuthInlet): + """Abstract subclass of OAuthInlet to specify GitHub Form""" + + form = "passbook.channels.in_oauth.forms.GitHubOAuthInletForm" + + class Meta: + + abstract = True + verbose_name = _("GitHub OAuth Inlet") + verbose_name_plural = _("GitHub OAuth Inlets") + + +class TwitterOAuthInlet(OAuthInlet): + """Abstract subclass of OAuthInlet to specify Twitter Form""" + + form = "passbook.channels.in_oauth.forms.TwitterOAuthInletForm" + + class Meta: + + abstract = True + verbose_name = _("Twitter OAuth Inlet") + verbose_name_plural = _("Twitter OAuth Inlets") + + +class FacebookOAuthInlet(OAuthInlet): + """Abstract subclass of OAuthInlet to specify Facebook Form""" + + form = "passbook.channels.in_oauth.forms.FacebookOAuthInletForm" + + class Meta: + + abstract = True + verbose_name = _("Facebook OAuth Inlet") + verbose_name_plural = _("Facebook OAuth Inlets") + + +class DiscordOAuthInlet(OAuthInlet): + """Abstract subclass of OAuthInlet to specify Discord Form""" + + form = "passbook.channels.in_oauth.forms.DiscordOAuthInletForm" + + class Meta: + + abstract = True + verbose_name = _("Discord OAuth Inlet") + verbose_name_plural = _("Discord OAuth Inlets") + + +class GoogleOAuthInlet(OAuthInlet): + """Abstract subclass of OAuthInlet to specify Google Form""" + + form = "passbook.channels.in_oauth.forms.GoogleOAuthInletForm" + + class Meta: + + abstract = True + verbose_name = _("Google OAuth Inlet") + verbose_name_plural = _("Google OAuth Inlets") + + +class AzureADOAuthInlet(OAuthInlet): + """Abstract subclass of OAuthInlet to specify AzureAD Form""" + + form = "passbook.channels.in_oauth.forms.AzureADOAuthInletForm" + + class Meta: + + abstract = True + verbose_name = _("Azure AD OAuth Inlet") + verbose_name_plural = _("Azure AD OAuth Inlets") + + +class UserOAuthInletConnection(UserInletConnection): + """Authorized remote OAuth inlet.""" + + identifier = models.CharField(max_length=255) + access_token = models.TextField(blank=True, null=True, default=None) + + def save(self, *args, **kwargs): + self.access_token = self.access_token or None + super().save(*args, **kwargs) + + @property + def api_client(self): + """Get API Client""" + return get_client(self.inlet, self.access_token or "") + + class Meta: + + verbose_name = _("User OAuth Inlet Connection") + verbose_name_plural = _("User OAuth Inlet Connections") diff --git a/passbook/channels/in_oauth/settings.py b/passbook/channels/in_oauth/settings.py new file mode 100644 index 000000000..619d2f89e --- /dev/null +++ b/passbook/channels/in_oauth/settings.py @@ -0,0 +1,15 @@ +"""Oauth2 Client Settings""" + +AUTHENTICATION_BACKENDS = [ + "passbook.channels.in_oauth.backends.AuthorizedServiceBackend", +] + +PASSBOOK_SOURCES_OAUTH_TYPES = [ + "passbook.channels.in_oauth.types.discord", + "passbook.channels.in_oauth.types.facebook", + "passbook.channels.in_oauth.types.github", + "passbook.channels.in_oauth.types.google", + "passbook.channels.in_oauth.types.reddit", + "passbook.channels.in_oauth.types.twitter", + "passbook.channels.in_oauth.types.azure_ad", +] diff --git a/passbook/sources/oauth/templates/oauth_client/user.html b/passbook/channels/in_oauth/templates/oauth_client/user.html similarity index 61% rename from passbook/sources/oauth/templates/oauth_client/user.html rename to passbook/channels/in_oauth/templates/oauth_client/user.html index b216a3849..91ee51f0f 100644 --- a/passbook/sources/oauth/templates/oauth_client/user.html +++ b/passbook/channels/in_oauth/templates/oauth_client/user.html @@ -9,12 +9,12 @@
{% if connections.exists %}

{% trans 'Connected.' %}

- + {% trans 'Disconnect' %} {% else %}

Not connected.

- + {% trans 'Connect' %} {% endif %} diff --git a/passbook/providers/oauth/__init__.py b/passbook/channels/in_oauth/types/__init__.py similarity index 100% rename from passbook/providers/oauth/__init__.py rename to passbook/channels/in_oauth/types/__init__.py diff --git a/passbook/sources/oauth/types/azure_ad.py b/passbook/channels/in_oauth/types/azure_ad.py similarity index 55% rename from passbook/sources/oauth/types/azure_ad.py rename to passbook/channels/in_oauth/types/azure_ad.py index 0ac83f2c6..a1a4b2afb 100644 --- a/passbook/sources/oauth/types/azure_ad.py +++ b/passbook/channels/in_oauth/types/azure_ad.py @@ -1,19 +1,19 @@ """AzureAD OAuth2 Views""" import uuid -from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.utils import user_get_or_create -from passbook.sources.oauth.views.core import OAuthCallback +from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind +from passbook.channels.in_oauth.utils import user_get_or_create +from passbook.channels.in_oauth.views.core import OAuthCallback -@MANAGER.source(kind=RequestKind.callback, name="Azure AD") +@MANAGER.inlet(kind=RequestKind.callback, name="Azure AD") class AzureADOAuthCallback(OAuthCallback): """AzureAD OAuth2 Callback""" - def get_user_id(self, source, info): + def get_user_id(self, inlet, info): return uuid.UUID(info.get("objectId")).int - def get_or_create_user(self, source, access, info): + def get_or_create_user(self, inlet, access, info): user_data = { "username": info.get("displayName"), "email": info.get("mail", None) or info.get("otherMails")[0], diff --git a/passbook/sources/oauth/types/discord.py b/passbook/channels/in_oauth/types/discord.py similarity index 55% rename from passbook/sources/oauth/types/discord.py rename to passbook/channels/in_oauth/types/discord.py index f82089cf5..0013fcf1f 100644 --- a/passbook/sources/oauth/types/discord.py +++ b/passbook/channels/in_oauth/types/discord.py @@ -1,24 +1,24 @@ """Discord OAuth Views""" -from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.utils import user_get_or_create -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind +from passbook.channels.in_oauth.utils import user_get_or_create +from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect -@MANAGER.source(kind=RequestKind.redirect, name="Discord") +@MANAGER.inlet(kind=RequestKind.redirect, name="Discord") class DiscordOAuthRedirect(OAuthRedirect): """Discord OAuth2 Redirect""" - def get_additional_parameters(self, source): + def get_additional_parameters(self, inlet): return { "scope": "email identify", } -@MANAGER.source(kind=RequestKind.callback, name="Discord") +@MANAGER.inlet(kind=RequestKind.callback, name="Discord") class DiscordOAuth2Callback(OAuthCallback): """Discord OAuth2 Callback""" - def get_or_create_user(self, source, access, info): + def get_or_create_user(self, inlet, access, info): user_data = { "username": info.get("username"), "email": info.get("email", "None"), diff --git a/passbook/sources/oauth/types/facebook.py b/passbook/channels/in_oauth/types/facebook.py similarity index 53% rename from passbook/sources/oauth/types/facebook.py rename to passbook/channels/in_oauth/types/facebook.py index 8b20cf3ee..d49995c34 100644 --- a/passbook/sources/oauth/types/facebook.py +++ b/passbook/channels/in_oauth/types/facebook.py @@ -1,24 +1,24 @@ """Facebook OAuth Views""" -from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.utils import user_get_or_create -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind +from passbook.channels.in_oauth.utils import user_get_or_create +from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect -@MANAGER.source(kind=RequestKind.redirect, name="Facebook") +@MANAGER.inlet(kind=RequestKind.redirect, name="Facebook") class FacebookOAuthRedirect(OAuthRedirect): """Facebook OAuth2 Redirect""" - def get_additional_parameters(self, source): + def get_additional_parameters(self, inlet): return { "scope": "email", } -@MANAGER.source(kind=RequestKind.callback, name="Facebook") +@MANAGER.inlet(kind=RequestKind.callback, name="Facebook") class FacebookOAuth2Callback(OAuthCallback): """Facebook OAuth2 Callback""" - def get_or_create_user(self, source, access, info): + def get_or_create_user(self, inlet, access, info): user_data = { "username": info.get("name"), "email": info.get("email", ""), diff --git a/passbook/sources/oauth/types/github.py b/passbook/channels/in_oauth/types/github.py similarity index 53% rename from passbook/sources/oauth/types/github.py rename to passbook/channels/in_oauth/types/github.py index 174fe046f..50a7ce000 100644 --- a/passbook/sources/oauth/types/github.py +++ b/passbook/channels/in_oauth/types/github.py @@ -1,14 +1,14 @@ """GitHub OAuth Views""" -from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.utils import user_get_or_create -from passbook.sources.oauth.views.core import OAuthCallback +from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind +from passbook.channels.in_oauth.utils import user_get_or_create +from passbook.channels.in_oauth.views.core import OAuthCallback -@MANAGER.source(kind=RequestKind.callback, name="GitHub") +@MANAGER.inlet(kind=RequestKind.callback, name="GitHub") class GitHubOAuth2Callback(OAuthCallback): """GitHub OAuth2 Callback""" - def get_or_create_user(self, source, access, info): + def get_or_create_user(self, inlet, access, info): user_data = { "username": info.get("login"), "email": info.get("email", ""), diff --git a/passbook/sources/oauth/types/google.py b/passbook/channels/in_oauth/types/google.py similarity index 54% rename from passbook/sources/oauth/types/google.py rename to passbook/channels/in_oauth/types/google.py index 4c721f438..9098a9849 100644 --- a/passbook/sources/oauth/types/google.py +++ b/passbook/channels/in_oauth/types/google.py @@ -1,24 +1,24 @@ """Google OAuth Views""" -from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.utils import user_get_or_create -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind +from passbook.channels.in_oauth.utils import user_get_or_create +from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect -@MANAGER.source(kind=RequestKind.redirect, name="Google") +@MANAGER.inlet(kind=RequestKind.redirect, name="Google") class GoogleOAuthRedirect(OAuthRedirect): """Google OAuth2 Redirect""" - def get_additional_parameters(self, source): + def get_additional_parameters(self, inlet): return { "scope": "email profile", } -@MANAGER.source(kind=RequestKind.callback, name="Google") +@MANAGER.inlet(kind=RequestKind.callback, name="Google") class GoogleOAuth2Callback(OAuthCallback): """Google OAuth2 Callback""" - def get_or_create_user(self, source, access, info): + def get_or_create_user(self, inlet, access, info): user_data = { "username": info.get("email"), "email": info.get("email", ""), diff --git a/passbook/sources/oauth/types/manager.py b/passbook/channels/in_oauth/types/manager.py similarity index 51% rename from passbook/sources/oauth/types/manager.py rename to passbook/channels/in_oauth/types/manager.py index 76d734d87..34f6bd818 100644 --- a/passbook/sources/oauth/types/manager.py +++ b/passbook/channels/in_oauth/types/manager.py @@ -1,10 +1,10 @@ -"""Source type manager""" +"""Inlet type manager""" from enum import Enum from django.utils.text import slugify from structlog import get_logger -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect LOGGER = get_logger() @@ -16,21 +16,21 @@ class RequestKind(Enum): redirect = "redirect" -class SourceTypeManager: - """Manager to hold all Source types.""" +class InletTypeManager: + """Manager to hold all Inlet types.""" - __source_types = {} + __inlet_types = {} __names = [] - def source(self, kind, name): + def inlet(self, kind, name): """Class decorator to register classes inline.""" def inner_wrapper(cls): - if kind not in self.__source_types: - self.__source_types[kind] = {} - self.__source_types[kind][name.lower()] = cls + if kind not in self.__inlet_types: + self.__inlet_types[kind] = {} + self.__inlet_types[kind][name.lower()] = cls self.__names.append(name) - LOGGER.debug("Registered source", source_class=cls.__name__, kind=kind) + LOGGER.debug("Registered inlet", inlet_class=cls.__name__, kind=kind) return cls return inner_wrapper @@ -39,11 +39,11 @@ class SourceTypeManager: """Get list of tuples of all registered names""" return [(slugify(x), x) for x in set(self.__names)] - def find(self, source, kind): - """Find fitting Source Type""" - if kind in self.__source_types: - if source.provider_type in self.__source_types[kind]: - return self.__source_types[kind][source.provider_type] + def find(self, inlet, kind): + """Find fitting Inlet Type""" + if kind in self.__inlet_types: + if inlet.provider_type in self.__inlet_types[kind]: + return self.__inlet_types[kind][inlet.provider_type] # Return defaults if kind == RequestKind.callback: return OAuthCallback @@ -52,4 +52,4 @@ class SourceTypeManager: raise KeyError -MANAGER = SourceTypeManager() +MANAGER = InletTypeManager() diff --git a/passbook/sources/oauth/types/reddit.py b/passbook/channels/in_oauth/types/reddit.py similarity index 61% rename from passbook/sources/oauth/types/reddit.py rename to passbook/channels/in_oauth/types/reddit.py index 8384d6435..ef2b3fd6a 100644 --- a/passbook/sources/oauth/types/reddit.py +++ b/passbook/channels/in_oauth/types/reddit.py @@ -1,17 +1,17 @@ """Reddit OAuth Views""" from requests.auth import HTTPBasicAuth -from passbook.sources.oauth.clients import OAuth2Client -from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.utils import user_get_or_create -from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect +from passbook.channels.in_oauth.clients import OAuth2Client +from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind +from passbook.channels.in_oauth.utils import user_get_or_create +from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect -@MANAGER.source(kind=RequestKind.redirect, name="reddit") +@MANAGER.inlet(kind=RequestKind.redirect, name="reddit") class RedditOAuthRedirect(OAuthRedirect): """Reddit OAuth2 Redirect""" - def get_additional_parameters(self, source): + def get_additional_parameters(self, inlet): return { "scope": "identity", "duration": "permanent", @@ -23,19 +23,19 @@ class RedditOAuth2Client(OAuth2Client): def get_access_token(self, request, callback=None, **request_kwargs): "Fetch access token from callback request." - auth = HTTPBasicAuth(self.source.consumer_key, self.source.consumer_secret) + auth = HTTPBasicAuth(self.inlet.consumer_key, self.inlet.consumer_secret) return super(RedditOAuth2Client, self).get_access_token( request, callback, auth=auth ) -@MANAGER.source(kind=RequestKind.callback, name="reddit") +@MANAGER.inlet(kind=RequestKind.callback, name="reddit") class RedditOAuth2Callback(OAuthCallback): """Reddit OAuth2 Callback""" client_class = RedditOAuth2Client - def get_or_create_user(self, source, access, info): + def get_or_create_user(self, inlet, access, info): user_data = { "username": info.get("name"), "email": None, diff --git a/passbook/sources/oauth/types/twitter.py b/passbook/channels/in_oauth/types/twitter.py similarity index 54% rename from passbook/sources/oauth/types/twitter.py rename to passbook/channels/in_oauth/types/twitter.py index 4a4196f3c..49df752fb 100644 --- a/passbook/sources/oauth/types/twitter.py +++ b/passbook/channels/in_oauth/types/twitter.py @@ -1,14 +1,14 @@ """Twitter OAuth Views""" -from passbook.sources.oauth.types.manager import MANAGER, RequestKind -from passbook.sources.oauth.utils import user_get_or_create -from passbook.sources.oauth.views.core import OAuthCallback +from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind +from passbook.channels.in_oauth.utils import user_get_or_create +from passbook.channels.in_oauth.views.core import OAuthCallback -@MANAGER.source(kind=RequestKind.callback, name="Twitter") +@MANAGER.inlet(kind=RequestKind.callback, name="Twitter") class TwitterOAuthCallback(OAuthCallback): """Twitter OAuth2 Callback""" - def get_or_create_user(self, source, access, info): + def get_or_create_user(self, inlet, access, info): user_data = { "username": info.get("screen_name"), "email": info.get("email", ""), diff --git a/passbook/sources/oauth/urls.py b/passbook/channels/in_oauth/urls.py similarity index 64% rename from passbook/sources/oauth/urls.py rename to passbook/channels/in_oauth/urls.py index c4dde11df..2e956d363 100644 --- a/passbook/sources/oauth/urls.py +++ b/passbook/channels/in_oauth/urls.py @@ -2,27 +2,27 @@ from django.urls import path -from passbook.sources.oauth.types.manager import RequestKind -from passbook.sources.oauth.views import core, dispatcher, user +from passbook.channels.in_oauth.types.manager import RequestKind +from passbook.channels.in_oauth.views import core, dispatcher, user urlpatterns = [ path( - "login//", + "login//", dispatcher.DispatcherView.as_view(kind=RequestKind.redirect), name="oauth-client-login", ), path( - "callback//", + "callback//", dispatcher.DispatcherView.as_view(kind=RequestKind.callback), name="oauth-client-callback", ), path( - "disconnect//", + "disconnect//", core.DisconnectView.as_view(), name="oauth-client-disconnect", ), path( - "user//", + "user//", user.UserSettingsView.as_view(), name="oauth-client-user", ), diff --git a/passbook/sources/oauth/utils.py b/passbook/channels/in_oauth/utils.py similarity index 100% rename from passbook/sources/oauth/utils.py rename to passbook/channels/in_oauth/utils.py diff --git a/passbook/providers/oauth/migrations/__init__.py b/passbook/channels/in_oauth/views/__init__.py similarity index 100% rename from passbook/providers/oauth/migrations/__init__.py rename to passbook/channels/in_oauth/views/__init__.py diff --git a/passbook/sources/oauth/views/core.py b/passbook/channels/in_oauth/views/core.py similarity index 54% rename from passbook/sources/oauth/views/core.py rename to passbook/channels/in_oauth/views/core.py index 36e84d869..ffddf7052 100644 --- a/passbook/sources/oauth/views/core.py +++ b/passbook/channels/in_oauth/views/core.py @@ -13,6 +13,8 @@ from django.views.generic import RedirectView, View from structlog import get_logger from passbook.audit.models import Event, EventAction +from passbook.channels.in_oauth.clients import get_client +from passbook.channels.in_oauth.models import OAuthInlet, UserOAuthInletConnection from passbook.flows.models import Flow, FlowDesignation from passbook.flows.planner import ( PLAN_CONTEXT_PENDING_USER, @@ -21,8 +23,6 @@ from passbook.flows.planner import ( ) from passbook.flows.views import SESSION_KEY_PLAN from passbook.lib.utils.urls import redirect_with_qs -from passbook.sources.oauth.clients import get_client -from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND LOGGER = get_logger() @@ -30,49 +30,49 @@ LOGGER = get_logger() # pylint: disable=too-few-public-methods class OAuthClientMixin: - "Mixin for getting OAuth client for a source." + "Mixin for getting OAuth client for a inlet." client_class: Optional[Callable] = None - def get_client(self, source): - "Get instance of the OAuth client for this source." + def get_client(self, inlet): + "Get instance of the OAuth client for this inlet." if self.client_class is not None: # pylint: disable=not-callable - return self.client_class(source) - return get_client(source) + return self.client_class(inlet) + return get_client(inlet) class OAuthRedirect(OAuthClientMixin, RedirectView): - "Redirect user to OAuth source to enable access." + "Redirect user to OAuth inlet to enable access." permanent = False params = None # pylint: disable=unused-argument - def get_additional_parameters(self, source): - "Return additional redirect parameters for this source." + def get_additional_parameters(self, inlet): + "Return additional redirect parameters for this inlet." return self.params or {} - def get_callback_url(self, source): - "Return the callback url for this source." + def get_callback_url(self, inlet): + "Return the callback url for this inlet." return reverse( - "passbook_sources_oauth:oauth-client-callback", - kwargs={"source_slug": source.slug}, + "passbook_channels_in_oauth:oauth-client-callback", + kwargs={"inlet_slug": inlet.slug}, ) def get_redirect_url(self, **kwargs): - "Build redirect url for a given source." - slug = kwargs.get("source_slug", "") + "Build redirect url for a given inlet." + slug = kwargs.get("inlet_slug", "") try: - source = OAuthSource.objects.get(slug=slug) - except OAuthSource.DoesNotExist: - raise Http404("Unknown OAuth source '%s'." % slug) + inlet = OAuthInlet.objects.get(slug=slug) + except OAuthInlet.DoesNotExist: + raise Http404("Unknown OAuth inlet '%s'." % slug) else: - if not source.enabled: - raise Http404("source %s is not enabled." % slug) - client = self.get_client(source) - callback = self.get_callback_url(source) - params = self.get_additional_parameters(source) + if not inlet.enabled: + raise Http404("inlet %s is not enabled." % slug) + client = self.get_client(inlet) + callback = self.get_callback_url(inlet) + params = self.get_additional_parameters(inlet) return client.get_redirect_url( self.request, callback=callback, parameters=params ) @@ -81,85 +81,85 @@ class OAuthRedirect(OAuthClientMixin, RedirectView): class OAuthCallback(OAuthClientMixin, View): "Base OAuth callback view." - source_id = None - source = None + inlet_id = None + inlet = None def get(self, request, *_, **kwargs): """View Get handler""" - slug = kwargs.get("source_slug", "") + slug = kwargs.get("inlet_slug", "") try: - self.source = OAuthSource.objects.get(slug=slug) - except OAuthSource.DoesNotExist: - raise Http404("Unknown OAuth source '%s'." % slug) + self.inlet = OAuthInlet.objects.get(slug=slug) + except OAuthInlet.DoesNotExist: + raise Http404("Unknown OAuth inlet '%s'." % slug) else: - if not self.source.enabled: - raise Http404("source %s is not enabled." % slug) - client = self.get_client(self.source) - callback = self.get_callback_url(self.source) + if not self.inlet.enabled: + raise Http404("inlet %s is not enabled." % slug) + client = self.get_client(self.inlet) + callback = self.get_callback_url(self.inlet) # Fetch access token token = client.get_access_token(self.request, callback=callback) if token is None: return self.handle_login_failure( - self.source, "Could not retrieve token." + self.inlet, "Could not retrieve token." ) if "error" in token: - return self.handle_login_failure(self.source, token["error"]) + return self.handle_login_failure(self.inlet, token["error"]) # Fetch profile info info = client.get_profile_info(token) if info is None: return self.handle_login_failure( - self.source, "Could not retrieve profile." + self.inlet, "Could not retrieve profile." ) - identifier = self.get_user_id(self.source, info) + identifier = self.get_user_id(self.inlet, info) if identifier is None: - return self.handle_login_failure(self.source, "Could not determine id.") + return self.handle_login_failure(self.inlet, "Could not determine id.") # Get or create access record defaults = { "access_token": token.get("access_token"), } - existing = UserOAuthSourceConnection.objects.filter( - source=self.source, identifier=identifier + existing = UserOAuthInletConnection.objects.filter( + inlet=self.inlet, identifier=identifier ) if existing.exists(): connection = existing.first() connection.access_token = token.get("access_token") - UserOAuthSourceConnection.objects.filter(pk=connection.pk).update( + UserOAuthInletConnection.objects.filter(pk=connection.pk).update( **defaults ) else: - connection = UserOAuthSourceConnection( - source=self.source, + connection = UserOAuthInletConnection( + inlet=self.inlet, identifier=identifier, access_token=token.get("access_token"), ) user = authenticate( - source=self.source, identifier=identifier, request=request + inlet=self.inlet, identifier=identifier, request=request ) if user is None: - LOGGER.debug("Handling new user", source=self.source) - return self.handle_new_user(self.source, connection, info) - LOGGER.debug("Handling existing user", source=self.source) - return self.handle_existing_user(self.source, user, connection, info) + LOGGER.debug("Handling new user", inlet=self.inlet) + return self.handle_new_user(self.inlet, connection, info) + LOGGER.debug("Handling existing user", inlet=self.inlet) + return self.handle_existing_user(self.inlet, user, connection, info) # pylint: disable=unused-argument - def get_callback_url(self, source): + def get_callback_url(self, inlet): "Return callback url if different than the current url." return False # pylint: disable=unused-argument - def get_error_redirect(self, source, reason): + def get_error_redirect(self, inlet, reason): "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, inlet, access, info): "Create a shell auth.User." raise NotImplementedError() # pylint: disable=unused-argument - def get_user_id(self, source, info): + def get_user_id(self, inlet, info): "Return unique identifier from the profile info." - id_key = self.source_id or "id" + id_key = self.inlet_id or "id" result = info try: for key in id_key.split("."): @@ -168,10 +168,10 @@ class OAuthCallback(OAuthClientMixin, View): except KeyError: return None - def handle_login(self, user, source, access): + def handle_login(self, user, inlet, access): """Prepare Authentication Plan, redirect user FlowExecutor""" user = authenticate( - source=access.source, identifier=access.identifier, request=self.request + inlet=access.inlet, identifier=access.identifier, request=self.request ) # 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) @@ -186,24 +186,24 @@ class OAuthCallback(OAuthClientMixin, View): ) # pylint: disable=unused-argument - def handle_existing_user(self, source, user, access, info): + def handle_existing_user(self, inlet, user, access, info): "Login user and redirect." messages.success( self.request, _( - "Successfully authenticated with %(source)s!" - % {"source": self.source.name} + "Successfully authenticated with %(inlet)s!" + % {"inlet": self.inlet.name} ), ) - return self.handle_login(user, source, access) + return self.handle_login(user, inlet, access) - def handle_login_failure(self, source, reason): + def handle_login_failure(self, inlet, reason): "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)) + return redirect(self.get_error_redirect(inlet, reason)) - def handle_new_user(self, source, access, info): + def handle_new_user(self, inlet, access, info): "Create a shell auth.User and redirect." was_authenticated = False if self.request.user.is_authenticated: @@ -211,52 +211,52 @@ class OAuthCallback(OAuthClientMixin, View): user = self.request.user was_authenticated = True else: - user = self.get_or_create_user(source, access, info) + user = self.get_or_create_user(inlet, access, info) access.user = user access.save() - UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user) + UserOAuthInletConnection.objects.filter(pk=access.pk).update(user=user) Event.new( - EventAction.CUSTOM, message="Linked OAuth Source", source=source + EventAction.CUSTOM, message="Linked OAuth Inlet", inlet=inlet ).from_http(self.request) if was_authenticated: messages.success( self.request, - _("Successfully linked %(source)s!" % {"source": self.source.name}), + _("Successfully linked %(inlet)s!" % {"inlet": self.inlet.name}), ) return redirect( reverse( - "passbook_sources_oauth:oauth-client-user", - kwargs={"source_slug": self.source.slug}, + "passbook_channels_in_oauth:oauth-client-user", + kwargs={"inlet_slug": self.inlet.slug}, ) ) # User was not authenticated, new user has been created user = authenticate( - source=access.source, identifier=access.identifier, request=self.request + inlet=access.inlet, identifier=access.identifier, request=self.request ) messages.success( self.request, _( - "Successfully authenticated with %(source)s!" - % {"source": self.source.name} + "Successfully authenticated with %(inlet)s!" + % {"inlet": self.inlet.name} ), ) - return self.handle_login(user, source, access) + return self.handle_login(user, inlet, access) class DisconnectView(LoginRequiredMixin, View): - """Delete connection with source""" + """Delete connection with inlet""" - source = None + inlet = None aas = None - def dispatch(self, request, source_slug): - self.source = get_object_or_404(OAuthSource, slug=source_slug) + def dispatch(self, request, inlet_slug): + self.inlet = get_object_or_404(OAuthInlet, slug=inlet_slug) self.aas = get_object_or_404( - UserOAuthSourceConnection, source=self.source, user=request.user + UserOAuthInletConnection, inlet=self.inlet, user=request.user ) - return super().dispatch(request, source_slug) + return super().dispatch(request, inlet_slug) - def post(self, request, source_slug): + def post(self, request, inlet_slug): """Delete connection object""" if "confirmdelete" in request.POST: # User confirmed deletion @@ -264,23 +264,23 @@ class DisconnectView(LoginRequiredMixin, View): messages.success(request, _("Connection successfully deleted")) return redirect( reverse( - "passbook_sources_oauth:oauth-client-user", - kwargs={"source_slug": self.source.slug}, + "passbook_channels_in_oauth:oauth-client-user", + kwargs={"inlet_slug": self.inlet.slug}, ) ) - return self.get(request, source_slug) + return self.get(request, inlet_slug) # pylint: disable=unused-argument - def get(self, request, source_slug): + def get(self, request, inlet_slug): """Show delete form""" return render( request, "generic/delete.html", { - "object": self.source, + "object": self.inlet, "delete_url": reverse( - "passbook_sources_oauth:oauth-client-disconnect", - kwargs={"source_slug": self.source.slug,}, + "passbook_channels_in_oauth:oauth-client-disconnect", + kwargs={"inlet_slug": self.inlet.slug,}, ), }, ) diff --git a/passbook/sources/oauth/views/dispatcher.py b/passbook/channels/in_oauth/views/dispatcher.py similarity index 55% rename from passbook/sources/oauth/views/dispatcher.py rename to passbook/channels/in_oauth/views/dispatcher.py index a7ce9024d..b0c29a6d3 100644 --- a/passbook/sources/oauth/views/dispatcher.py +++ b/passbook/channels/in_oauth/views/dispatcher.py @@ -3,8 +3,8 @@ from django.http import Http404 from django.shortcuts import get_object_or_404 from django.views import View -from passbook.sources.oauth.models import OAuthSource -from passbook.sources.oauth.types.manager import MANAGER, RequestKind +from passbook.channels.in_oauth.models import OAuthInlet +from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind class DispatcherView(View): @@ -13,10 +13,10 @@ class DispatcherView(View): kind = "" def dispatch(self, *args, **kwargs): - """Find Source by slug and forward request""" - slug = kwargs.get("source_slug", None) + """Find Inlet by slug and forward request""" + slug = kwargs.get("inlet_slug", None) if not slug: raise Http404 - source = get_object_or_404(OAuthSource, slug=slug) - view = MANAGER.find(source, kind=RequestKind(self.kind)) + inlet = get_object_or_404(OAuthInlet, slug=slug) + view = MANAGER.find(inlet, kind=RequestKind(self.kind)) return view.as_view()(*args, **kwargs) diff --git a/passbook/sources/oauth/views/user.py b/passbook/channels/in_oauth/views/user.py similarity index 60% rename from passbook/sources/oauth/views/user.py rename to passbook/channels/in_oauth/views/user.py index f05ab2549..66e53ae28 100644 --- a/passbook/sources/oauth/views/user.py +++ b/passbook/channels/in_oauth/views/user.py @@ -3,7 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404 from django.views.generic import TemplateView -from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection +from passbook.channels.in_oauth.models import OAuthInlet, UserOAuthInletConnection class UserSettingsView(LoginRequiredMixin, TemplateView): @@ -12,10 +12,10 @@ class UserSettingsView(LoginRequiredMixin, TemplateView): template_name = "oauth_client/user.html" def get_context_data(self, **kwargs): - source = get_object_or_404(OAuthSource, slug=self.kwargs.get("source_slug")) - connections = UserOAuthSourceConnection.objects.filter( - user=self.request.user, source=source + inlet = get_object_or_404(OAuthInlet, slug=self.kwargs.get("inlet_slug")) + connections = UserOAuthInletConnection.objects.filter( + user=self.request.user, inlet=inlet ) - kwargs["source"] = source + kwargs["inlet"] = inlet kwargs["connections"] = connections return super().get_context_data(**kwargs) diff --git a/passbook/providers/oauth/views/__init__.py b/passbook/channels/in_saml/__init__.py similarity index 100% rename from passbook/providers/oauth/views/__init__.py rename to passbook/channels/in_saml/__init__.py diff --git a/passbook/channels/in_saml/api.py b/passbook/channels/in_saml/api.py new file mode 100644 index 000000000..2bc4286e4 --- /dev/null +++ b/passbook/channels/in_saml/api.py @@ -0,0 +1,28 @@ +"""SAMLInlet API Views""" +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from passbook.channels.in_saml.models import SAMLInlet + + +class SAMLInletSerializer(ModelSerializer): + """SAMLInlet Serializer""" + + class Meta: + + model = SAMLInlet + fields = [ + "pk", + "issuer", + "idp_url", + "idp_logout_url", + "auto_logout", + "signing_kp", + ] + + +class SAMLInletViewSet(ModelViewSet): + """SAMLInlet Viewset""" + + queryset = SAMLInlet.objects.all() + serializer_class = SAMLInletSerializer diff --git a/passbook/channels/in_saml/apps.py b/passbook/channels/in_saml/apps.py new file mode 100644 index 000000000..b895d9225 --- /dev/null +++ b/passbook/channels/in_saml/apps.py @@ -0,0 +1,12 @@ +"""Passbook SAML app config""" + +from django.apps import AppConfig + + +class PassbookInletSAMLConfig(AppConfig): + """passbook saml_idp app config""" + + name = "passbook.channels.in_saml" + label = "passbook_channels_in_saml" + verbose_name = "passbook Inlets.SAML" + mountpoint = "source/saml/" diff --git a/passbook/sources/saml/exceptions.py b/passbook/channels/in_saml/exceptions.py similarity index 100% rename from passbook/sources/saml/exceptions.py rename to passbook/channels/in_saml/exceptions.py diff --git a/passbook/sources/saml/forms.py b/passbook/channels/in_saml/forms.py similarity index 70% rename from passbook/sources/saml/forms.py rename to passbook/channels/in_saml/forms.py index cfc3bf2c5..be745dcc8 100644 --- a/passbook/sources/saml/forms.py +++ b/passbook/channels/in_saml/forms.py @@ -4,17 +4,17 @@ from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext as _ -from passbook.admin.forms.source import SOURCE_FORM_FIELDS -from passbook.sources.saml.models import SAMLSource +from passbook.admin.forms.inlet import INLET_FORM_FIELDS +from passbook.channels.in_saml.models import SAMLInlet -class SAMLSourceForm(forms.ModelForm): - """SAML Provider form""" +class SAMLInletForm(forms.ModelForm): + """SAML Inlet form""" class Meta: - model = SAMLSource - fields = SOURCE_FORM_FIELDS + [ + model = SAMLInlet + fields = INLET_FORM_FIELDS + [ "issuer", "idp_url", "idp_logout_url", diff --git a/passbook/channels/in_saml/migrations/0001_initial.py b/passbook/channels/in_saml/migrations/0001_initial.py new file mode 100644 index 000000000..a1ead667a --- /dev/null +++ b/passbook/channels/in_saml/migrations/0001_initial.py @@ -0,0 +1,68 @@ +# Generated by Django 3.0.5 on 2020-05-15 19:59 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("passbook_crypto", "0001_initial"), + ("passbook_core", "__first__"), + ] + + operations = [ + migrations.CreateModel( + name="SAMLInlet", + fields=[ + ( + "inlet_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="passbook_core.Inlet", + ), + ), + ( + "issuer", + models.TextField( + blank=True, + default=None, + help_text="Also known as Entity ID. Defaults the Metadata URL.", + verbose_name="Issuer", + ), + ), + ("idp_url", models.URLField(verbose_name="IDP URL")), + ( + "idp_logout_url", + models.URLField( + blank=True, + default=None, + null=True, + verbose_name="IDP Logout URL", + ), + ), + ("auto_logout", models.BooleanField(default=False)), + ( + "signing_kp", + models.ForeignKey( + default=None, + help_text="Certificate Key Pair of the IdP which Assertions are validated against.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="passbook_crypto.CertificateKeyPair", + ), + ), + ], + options={ + "verbose_name": "SAML Inlet", + "verbose_name_plural": "SAML Inlets", + }, + bases=("passbook_core.inlet",), + ), + ] diff --git a/passbook/providers/oidc/__init__.py b/passbook/channels/in_saml/migrations/__init__.py similarity index 100% rename from passbook/providers/oidc/__init__.py rename to passbook/channels/in_saml/migrations/__init__.py diff --git a/passbook/sources/saml/models.py b/passbook/channels/in_saml/models.py similarity index 75% rename from passbook/sources/saml/models.py rename to passbook/channels/in_saml/models.py index f98133ffc..f262efe27 100644 --- a/passbook/sources/saml/models.py +++ b/passbook/channels/in_saml/models.py @@ -3,13 +3,13 @@ from django.db import models from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from passbook.core.models import Source +from passbook.core.models import Inlet from passbook.core.types import UILoginButton from passbook.crypto.models import CertificateKeyPair -class SAMLSource(Source): - """SAML Source""" +class SAMLInlet(Inlet): + """SAML Inlet""" issuer = models.TextField( blank=True, @@ -34,14 +34,14 @@ class SAMLSource(Source): on_delete=models.SET_NULL, ) - form = "passbook.sources.saml.forms.SAMLSourceForm" + form = "passbook.channels.in_saml.forms.SAMLInletForm" @property def ui_login_button(self) -> UILoginButton: return UILoginButton( name=self.name, url=reverse_lazy( - "passbook_sources_saml:login", kwargs={"source_slug": self.slug} + "passbook_channels_in_saml:login", kwargs={"inlet_slug": self.slug} ), icon_path="", ) @@ -49,14 +49,14 @@ class SAMLSource(Source): @property def ui_additional_info(self) -> str: metadata_url = reverse_lazy( - "passbook_sources_saml:metadata", kwargs={"source_slug": self.slug} + "passbook_channels_in_saml:metadata", kwargs={"inlet_slug": self.slug} ) return f'Metadata Download' def __str__(self): - return f"SAML Source {self.name}" + return f"SAML Inlet {self.name}" class Meta: - verbose_name = _("SAML Source") - verbose_name_plural = _("SAML Sources") + verbose_name = _("SAML Inlet") + verbose_name_plural = _("SAML Inlets") diff --git a/passbook/providers/oidc/migrations/__init__.py b/passbook/channels/in_saml/processors/__init__.py similarity index 100% rename from passbook/providers/oidc/migrations/__init__.py rename to passbook/channels/in_saml/processors/__init__.py diff --git a/passbook/sources/saml/processors/base.py b/passbook/channels/in_saml/processors/base.py similarity index 87% rename from passbook/sources/saml/processors/base.py rename to passbook/channels/in_saml/processors/base.py index 5568d9286..61d8a46d7 100644 --- a/passbook/sources/saml/processors/base.py +++ b/passbook/channels/in_saml/processors/base.py @@ -1,4 +1,4 @@ -"""passbook saml source processor""" +"""passbook saml inlet processor""" from typing import TYPE_CHECKING, Optional from defusedxml import ElementTree @@ -6,13 +6,13 @@ from django.http import HttpRequest from signxml import XMLVerifier from structlog import get_logger -from passbook.core.models import User -from passbook.providers.saml.utils.encoding import decode_base64_and_inflate -from passbook.sources.saml.exceptions import ( +from passbook.channels.in_saml.exceptions import ( MissingSAMLResponse, UnsupportedNameIDFormat, ) -from passbook.sources.saml.models import SAMLSource +from passbook.channels.in_saml.models import SAMLInlet +from passbook.channels.out_saml.utils.encoding import decode_base64_and_inflate +from passbook.core.models import User LOGGER = get_logger() if TYPE_CHECKING: @@ -22,13 +22,13 @@ if TYPE_CHECKING: class Processor: """SAML Response Processor""" - _source: SAMLSource + _inlet: SAMLInlet _root: "Element" _root_xml: str - def __init__(self, source: SAMLSource): - self._source = source + def __init__(self, inlet: SAMLInlet): + self._inlet = inlet def parse(self, request: HttpRequest): """Check if `request` contains SAML Response data, parse and validate it.""" @@ -46,7 +46,7 @@ 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._inlet.signing_kp.certificate) def _get_email(self) -> Optional[str]: """ diff --git a/passbook/sources/saml/templates/saml/sp/login.html b/passbook/channels/in_saml/templates/saml/sp/login.html similarity index 100% rename from passbook/sources/saml/templates/saml/sp/login.html rename to passbook/channels/in_saml/templates/saml/sp/login.html diff --git a/passbook/sources/saml/templates/saml/sp/sso_single_logout.html b/passbook/channels/in_saml/templates/saml/sp/sso_single_logout.html similarity index 100% rename from passbook/sources/saml/templates/saml/sp/sso_single_logout.html rename to passbook/channels/in_saml/templates/saml/sp/sso_single_logout.html diff --git a/passbook/sources/saml/templates/saml/sp/xml/authn_request.xml b/passbook/channels/in_saml/templates/saml/sp/xml/authn_request.xml similarity index 100% rename from passbook/sources/saml/templates/saml/sp/xml/authn_request.xml rename to passbook/channels/in_saml/templates/saml/sp/xml/authn_request.xml diff --git a/passbook/sources/saml/templates/saml/sp/xml/signature.xml b/passbook/channels/in_saml/templates/saml/sp/xml/signature.xml similarity index 100% rename from passbook/sources/saml/templates/saml/sp/xml/signature.xml rename to passbook/channels/in_saml/templates/saml/sp/xml/signature.xml diff --git a/passbook/sources/saml/templates/saml/sp/xml/signed_info.xml b/passbook/channels/in_saml/templates/saml/sp/xml/signed_info.xml similarity index 100% rename from passbook/sources/saml/templates/saml/sp/xml/signed_info.xml rename to passbook/channels/in_saml/templates/saml/sp/xml/signed_info.xml diff --git a/passbook/sources/saml/templates/saml/sp/xml/sp_sso_descriptor.xml b/passbook/channels/in_saml/templates/saml/sp/xml/sp_sso_descriptor.xml similarity index 100% rename from passbook/sources/saml/templates/saml/sp/xml/sp_sso_descriptor.xml rename to passbook/channels/in_saml/templates/saml/sp/xml/sp_sso_descriptor.xml diff --git a/passbook/sources/saml/urls.py b/passbook/channels/in_saml/urls.py similarity index 80% rename from passbook/sources/saml/urls.py rename to passbook/channels/in_saml/urls.py index 22b0a8fd2..575d2b9c1 100644 --- a/passbook/sources/saml/urls.py +++ b/passbook/channels/in_saml/urls.py @@ -1,7 +1,7 @@ """saml sp urls""" from django.urls import path -from passbook.sources.saml.views import ACSView, InitiateView, MetadataView, SLOView +from passbook.channels.in_saml.views import ACSView, InitiateView, MetadataView, SLOView urlpatterns = [ path("/", InitiateView.as_view(), name="login"), diff --git a/passbook/channels/in_saml/utils.py b/passbook/channels/in_saml/utils.py new file mode 100644 index 000000000..1c2fb7eeb --- /dev/null +++ b/passbook/channels/in_saml/utils.py @@ -0,0 +1,20 @@ +"""saml sp helpers""" +from django.http import HttpRequest +from django.shortcuts import reverse + +from passbook.channels.in_saml.models import SAMLInlet + + +def get_issuer(request: HttpRequest, inlet: SAMLInlet) -> str: + """Get Inlet's Issuer, falling back to our Metadata URL if none is set""" + issuer = inlet.issuer + if issuer is None: + return build_full_url("metadata", request, inlet) + return issuer + + +def build_full_url(view: str, request: HttpRequest, inlet: SAMLInlet) -> str: + """Build Full ACS URL to be used in IDP""" + return request.build_absolute_uri( + reverse(f"passbook_channels_in_saml:{view}", kwargs={"inlet_slug": inlet.slug}) + ) diff --git a/passbook/sources/saml/views.py b/passbook/channels/in_saml/views.py similarity index 59% rename from passbook/sources/saml/views.py rename to passbook/channels/in_saml/views.py index 6dd38db93..fe939f058 100644 --- a/passbook/sources/saml/views.py +++ b/passbook/channels/in_saml/views.py @@ -7,36 +7,36 @@ from django.views import View from django.views.decorators.csrf import csrf_exempt from signxml.util import strip_pem_header -from passbook.lib.views import bad_request_message -from passbook.providers.saml.utils import get_random_id, render_xml -from passbook.providers.saml.utils.encoding import nice64 -from passbook.providers.saml.utils.time import get_time_string -from passbook.sources.saml.exceptions import ( +from passbook.channels.in_saml.exceptions import ( MissingSAMLResponse, UnsupportedNameIDFormat, ) -from passbook.sources.saml.models import SAMLSource -from passbook.sources.saml.processors.base import Processor -from passbook.sources.saml.utils import build_full_url, get_issuer -from passbook.sources.saml.xml_render import get_authnrequest_xml +from passbook.channels.in_saml.models import SAMLInlet +from passbook.channels.in_saml.processors.base import Processor +from passbook.channels.in_saml.utils import build_full_url, get_issuer +from passbook.channels.in_saml.xml_render import get_authnrequest_xml +from passbook.channels.out_saml.utils import get_random_id, render_xml +from passbook.channels.out_saml.utils.encoding import nice64 +from passbook.channels.out_saml.utils.time import get_time_string +from passbook.lib.views import bad_request_message class InitiateView(View): """Get the Form with SAML Request, which sends us to the IDP""" - def get(self, request: HttpRequest, source_slug: str) -> HttpResponse: + def get(self, request: HttpRequest, inlet_slug: str) -> HttpResponse: """Replies with an XHTML SSO Request.""" - source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) - if not source.enabled: + inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug) + if not inlet.enabled: raise Http404 sso_destination = request.GET.get("next", None) request.session["sso_destination"] = sso_destination parameters = { - "ACS_URL": build_full_url("acs", request, source), - "DESTINATION": source.idp_url, + "ACS_URL": build_full_url("acs", request, inlet), + "DESTINATION": inlet.idp_url, "AUTHN_REQUEST_ID": get_random_id(), "ISSUE_INSTANT": get_time_string(), - "ISSUER": get_issuer(request, source), + "ISSUER": get_issuer(request, inlet), } authn_req = get_authnrequest_xml(parameters, signed=False) _request = nice64(str.encode(authn_req)) @@ -44,10 +44,10 @@ class InitiateView(View): request, "saml/sp/login.html", { - "request_url": source.idp_url, + "request_url": inlet.idp_url, "request": _request, "token": sso_destination, - "source": source, + "inlet": inlet, }, ) @@ -56,12 +56,12 @@ class InitiateView(View): class ACSView(View): """AssertionConsumerService, consume assertion and log user in""" - def post(self, request: HttpRequest, source_slug: str) -> HttpResponse: + def post(self, request: HttpRequest, inlet_slug: str) -> HttpResponse: """Handles a POSTed SSO Assertion and logs the user in.""" - source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) - if not source.enabled: + inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug) + if not inlet.enabled: raise Http404 - processor = Processor(source) + processor = Processor(inlet) try: processor.parse(request) except MissingSAMLResponse as exc: @@ -78,37 +78,34 @@ class ACSView(View): class SLOView(View): """Single-Logout-View""" - def dispatch(self, request: HttpRequest, source_slug: str) -> HttpResponse: + def dispatch(self, request: HttpRequest, inlet_slug: str) -> HttpResponse: """Replies with an XHTML SSO Request.""" - source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) - if not source.enabled: + inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug) + if not inlet.enabled: raise Http404 logout(request) return render( request, "saml/sp/sso_single_logout.html", - { - "idp_logout_url": source.idp_logout_url, - "autosubmit": source.auto_logout, - }, + {"idp_logout_url": inlet.idp_logout_url, "autosubmit": inlet.auto_logout,}, ) class MetadataView(View): """Return XML Metadata for IDP""" - def dispatch(self, request: HttpRequest, source_slug: str) -> HttpResponse: + def dispatch(self, request: HttpRequest, inlet_slug: str) -> HttpResponse: """Replies with the XML Metadata SPSSODescriptor.""" - source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) - issuer = get_issuer(request, source) + inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug) + issuer = get_issuer(request, inlet) cert_stripped = strip_pem_header( - source.signing_kp.certificate_data.replace("\r", "") + inlet.signing_kp.certificate_data.replace("\r", "") ).replace("\n", "") return render_xml( request, "saml/sp/xml/sp_sso_descriptor.xml", { - "acs_url": build_full_url("acs", request, source), + "acs_url": build_full_url("acs", request, inlet), "issuer": issuer, "cert_public_key": cert_stripped, }, diff --git a/passbook/sources/saml/xml_render.py b/passbook/channels/in_saml/xml_render.py similarity index 89% rename from passbook/sources/saml/xml_render.py rename to passbook/channels/in_saml/xml_render.py index 6d6726b14..6328226df 100644 --- a/passbook/sources/saml/xml_render.py +++ b/passbook/channels/in_saml/xml_render.py @@ -1,8 +1,8 @@ """Functions for creating XML output.""" from structlog import get_logger +from passbook.channels.out_saml.utils.xml_signing import get_signature_xml from passbook.lib.utils.template import render_to_string -from passbook.providers.saml.utils.xml_signing import get_signature_xml LOGGER = get_logger() diff --git a/passbook/providers/saml/__init__.py b/passbook/channels/out_app_gw/__init__.py similarity index 100% rename from passbook/providers/saml/__init__.py rename to passbook/channels/out_app_gw/__init__.py diff --git a/passbook/providers/app_gw/api.py b/passbook/channels/out_app_gw/api.py similarity index 64% rename from passbook/providers/app_gw/api.py rename to passbook/channels/out_app_gw/api.py index c4694f8b9..a80ce629c 100644 --- a/passbook/providers/app_gw/api.py +++ b/passbook/channels/out_app_gw/api.py @@ -1,17 +1,17 @@ -"""ApplicationGatewayProvider API Views""" +"""ApplicationGatewayOutlet API Views""" from oauth2_provider.generators import generate_client_id, generate_client_secret from oidc_provider.models import Client from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet -from passbook.providers.app_gw.models import ApplicationGatewayProvider -from passbook.providers.oidc.api import OpenIDProviderSerializer +from passbook.channels.out_app_gw.models import ApplicationGatewayOutlet +from passbook.channels.out_oidc.api import OpenIDOutletSerializer -class ApplicationGatewayProviderSerializer(ModelSerializer): - """ApplicationGatewayProvider Serializer""" +class ApplicationGatewayOutletSerializer(ModelSerializer): + """ApplicationGatewayOutlet Serializer""" - client = OpenIDProviderSerializer() + client = OpenIDOutletSerializer() def create(self, validated_data): instance = super().create(validated_data) @@ -33,13 +33,13 @@ class ApplicationGatewayProviderSerializer(ModelSerializer): class Meta: - model = ApplicationGatewayProvider + model = ApplicationGatewayOutlet fields = ["pk", "name", "internal_host", "external_host", "client"] read_only_fields = ["client"] -class ApplicationGatewayProviderViewSet(ModelViewSet): - """ApplicationGatewayProvider Viewset""" +class ApplicationGatewayOutletViewSet(ModelViewSet): + """ApplicationGatewayOutlet Viewset""" - queryset = ApplicationGatewayProvider.objects.all() - serializer_class = ApplicationGatewayProviderSerializer + queryset = ApplicationGatewayOutlet.objects.all() + serializer_class = ApplicationGatewayOutletSerializer diff --git a/passbook/providers/app_gw/apps.py b/passbook/channels/out_app_gw/apps.py similarity index 58% rename from passbook/providers/app_gw/apps.py rename to passbook/channels/out_app_gw/apps.py index a1702dcf9..0b760da89 100644 --- a/passbook/providers/app_gw/apps.py +++ b/passbook/channels/out_app_gw/apps.py @@ -5,7 +5,7 @@ from django.apps import AppConfig class PassbookApplicationApplicationGatewayConfig(AppConfig): """passbook app_gw app""" - name = "passbook.providers.app_gw" - label = "passbook_providers_app_gw" - verbose_name = "passbook Providers.Application Security Gateway" + name = "passbook.channels.out_app_gw" + label = "passbook_channels_out_app_gw" + verbose_name = "passbook Outlets.Application Security Gateway" mountpoint = "application/gateway/" diff --git a/passbook/providers/app_gw/forms.py b/passbook/channels/out_app_gw/forms.py similarity index 86% rename from passbook/providers/app_gw/forms.py rename to passbook/channels/out_app_gw/forms.py index ad4c7fc60..c35db6fe4 100644 --- a/passbook/providers/app_gw/forms.py +++ b/passbook/channels/out_app_gw/forms.py @@ -3,11 +3,11 @@ from django import forms from oauth2_provider.generators import generate_client_id, generate_client_secret from oidc_provider.models import Client, ResponseType -from passbook.providers.app_gw.models import ApplicationGatewayProvider +from passbook.channels.out_app_gw.models import ApplicationGatewayOutlet -class ApplicationGatewayProviderForm(forms.ModelForm): - """Security Gateway Provider form""" +class ApplicationGatewayOutletForm(forms.ModelForm): + """Security Gateway Outlet form""" def save(self, *args, **kwargs): if not self.instance.pk: @@ -31,7 +31,7 @@ class ApplicationGatewayProviderForm(forms.ModelForm): class Meta: - model = ApplicationGatewayProvider + model = ApplicationGatewayOutlet fields = ["name", "internal_host", "external_host"] widgets = { "name": forms.TextInput(), diff --git a/passbook/providers/app_gw/migrations/0003_applicationgatewayprovider.py b/passbook/channels/out_app_gw/migrations/0001_initial.py similarity index 65% rename from passbook/providers/app_gw/migrations/0003_applicationgatewayprovider.py rename to passbook/channels/out_app_gw/migrations/0001_initial.py index 36747a09c..b82637e44 100644 --- a/passbook/providers/app_gw/migrations/0003_applicationgatewayprovider.py +++ b/passbook/channels/out_app_gw/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.7 on 2019-11-11 17:08 +# Generated by Django 3.0.5 on 2020-05-15 19:59 import django.db.models.deletion from django.db import migrations, models @@ -9,28 +9,28 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("passbook_core", "0005_merge_20191025_2022"), + ("passbook_core", "__first__"), ("oidc_provider", "0026_client_multiple_response_types"), - ("passbook_providers_app_gw", "0002_auto_20191111_1703"), ] operations = [ migrations.CreateModel( - name="ApplicationGatewayProvider", + name="ApplicationGatewayOutlet", fields=[ ( - "provider_ptr", + "outlet_ptr", models.OneToOneField( auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, - to="passbook_core.Provider", + to="passbook_core.Outlet", ), ), ("name", models.TextField()), - ("host", models.TextField()), + ("internal_host", models.TextField()), + ("external_host", models.TextField()), ( "client", models.ForeignKey( @@ -40,9 +40,9 @@ class Migration(migrations.Migration): ), ], options={ - "verbose_name": "Application Gateway Provider", - "verbose_name_plural": "Application Gateway Providers", + "verbose_name": "Application Gateway Outlet", + "verbose_name_plural": "Application Gateway Outlets", }, - bases=("passbook_core.provider",), + bases=("passbook_core.outlet",), ), ] diff --git a/passbook/providers/saml/migrations/__init__.py b/passbook/channels/out_app_gw/migrations/__init__.py similarity index 100% rename from passbook/providers/saml/migrations/__init__.py rename to passbook/channels/out_app_gw/migrations/__init__.py diff --git a/passbook/providers/app_gw/models.py b/passbook/channels/out_app_gw/models.py similarity index 69% rename from passbook/providers/app_gw/models.py rename to passbook/channels/out_app_gw/models.py index 664c75f8b..ff049bfd1 100644 --- a/passbook/providers/app_gw/models.py +++ b/passbook/channels/out_app_gw/models.py @@ -9,12 +9,12 @@ from django.utils.translation import gettext as _ from oidc_provider.models import Client from passbook import __version__ -from passbook.core.models import Provider +from passbook.core.models import Outlet from passbook.lib.utils.template import render_to_string -class ApplicationGatewayProvider(Provider): - """This provider uses oauth2_proxy with the OIDC Provider.""" +class ApplicationGatewayOutlet(Outlet): + """This outlet uses oauth2_proxy with the OIDC Outlet.""" name = models.TextField() internal_host = models.TextField() @@ -22,7 +22,7 @@ class ApplicationGatewayProvider(Provider): client = models.ForeignKey(Client, on_delete=models.CASCADE) - form = "passbook.providers.app_gw.forms.ApplicationGatewayProviderForm" + form = "passbook.channels.out_app_gw.forms.ApplicationGatewayOutletForm" def html_setup_urls(self, request: HttpRequest) -> Optional[str]: """return template and context modal with URLs for authorize, token, openid-config, etc""" @@ -32,7 +32,7 @@ class ApplicationGatewayProvider(Provider): ) return render_to_string( "app_gw/setup_modal.html", - {"provider": self, "cookie_secret": cookie_secret, "version": __version__}, + {"outlet": self, "cookie_secret": cookie_secret, "version": __version__}, ) def __str__(self): @@ -40,5 +40,5 @@ class ApplicationGatewayProvider(Provider): class Meta: - verbose_name = _("Application Gateway Provider") - verbose_name_plural = _("Application Gateway Providers") + verbose_name = _("Application Gateway Outlet") + verbose_name_plural = _("Application Gateway Outlets") diff --git a/passbook/providers/saml/processors/__init__.py b/passbook/channels/out_app_gw/provider/__init__.py similarity index 100% rename from passbook/providers/saml/processors/__init__.py rename to passbook/channels/out_app_gw/provider/__init__.py diff --git a/passbook/providers/saml/tests/__init__.py b/passbook/channels/out_app_gw/provider/kubernetes/__init__.py similarity index 100% rename from passbook/providers/saml/tests/__init__.py rename to passbook/channels/out_app_gw/provider/kubernetes/__init__.py diff --git a/passbook/providers/app_gw/templates/app_gw/docker-compose.yml b/passbook/channels/out_app_gw/templates/app_gw/docker-compose.yml similarity index 100% rename from passbook/providers/app_gw/templates/app_gw/docker-compose.yml rename to passbook/channels/out_app_gw/templates/app_gw/docker-compose.yml diff --git a/passbook/providers/app_gw/templates/app_gw/k8s-manifest.yaml b/passbook/channels/out_app_gw/templates/app_gw/k8s-manifest.yaml similarity index 100% rename from passbook/providers/app_gw/templates/app_gw/k8s-manifest.yaml rename to passbook/channels/out_app_gw/templates/app_gw/k8s-manifest.yaml diff --git a/passbook/providers/app_gw/templates/app_gw/setup_modal.html b/passbook/channels/out_app_gw/templates/app_gw/setup_modal.html similarity index 96% rename from passbook/providers/app_gw/templates/app_gw/setup_modal.html rename to passbook/channels/out_app_gw/templates/app_gw/setup_modal.html index db8000332..4b5605f06 100644 --- a/passbook/providers/app_gw/templates/app_gw/setup_modal.html +++ b/passbook/channels/out_app_gw/templates/app_gw/setup_modal.html @@ -42,7 +42,7 @@

{% trans 'Setup with Kubernetes' %}

{% trans 'Download the manifest to create the Gatekeeper deployment and service:' %}

- {% trans 'Here' %} + {% trans 'Here' %}

{% trans 'Afterwards, add the following annotations to the Ingress you want to secure:' %}