*: providers and sources -> channels, PolicyModel to PolicyBindingModel that uses custom M2M through
This commit is contained in:
		
							
								
								
									
										4
									
								
								passbook/admin/forms/inlet.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								passbook/admin/forms/inlet.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| """passbook core inlet form fields""" | ||||
|  | ||||
| INLET_FORM_FIELDS = ["name", "slug", "enabled"] | ||||
| INLET_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"] | ||||
| @ -1,4 +0,0 @@ | ||||
| """passbook core source form fields""" | ||||
|  | ||||
| SOURCE_FORM_FIELDS = ["name", "slug", "enabled"] | ||||
| SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"] | ||||
| @ -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/<uuid:pk>/update/", | ||||
|         sources.SourceUpdateView.as_view(), | ||||
|         name="source-update", | ||||
|         "inlets/<uuid:pk>/update/", | ||||
|         inlets.InletUpdateView.as_view(), | ||||
|         name="inlet-update", | ||||
|     ), | ||||
|     path( | ||||
|         "sources/<uuid:pk>/delete/", | ||||
|         sources.SourceDeleteView.as_view(), | ||||
|         name="source-delete", | ||||
|         "inlets/<uuid:pk>/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/<uuid:pk>/update/", | ||||
|         policy.PolicyUpdateView.as_view(), | ||||
|         policies.PolicyUpdateView.as_view(), | ||||
|         name="policy-update", | ||||
|     ), | ||||
|     path( | ||||
|         "policies/<uuid:pk>/delete/", | ||||
|         policy.PolicyDeleteView.as_view(), | ||||
|         policies.PolicyDeleteView.as_view(), | ||||
|         name="policy-delete", | ||||
|     ), | ||||
|     path( | ||||
|         "policies/<uuid:pk>/test/", policy.PolicyTestView.as_view(), name="policy-test" | ||||
|         "policies/<uuid:pk>/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/<int:pk>/update/", | ||||
|         outlets.OutletUpdateView.as_view(), | ||||
|         name="outlet-update", | ||||
|     ), | ||||
|     path( | ||||
|         "providers/<int:pk>/update/", | ||||
|         providers.ProviderUpdateView.as_view(), | ||||
|         name="provider-update", | ||||
|     ), | ||||
|     path( | ||||
|         "providers/<int:pk>/delete/", | ||||
|         providers.ProviderDeleteView.as_view(), | ||||
|         name="provider-delete", | ||||
|         "outlets/<int:pk>/delete/", | ||||
|         outlets.OutletDeleteView.as_view(), | ||||
|         name="outlet-delete", | ||||
|     ), | ||||
|     # Stages | ||||
|     path("stages/", stages.StageListView.as_view(), name="stages"), | ||||
|  | ||||
| @ -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): | ||||
| @ -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): | ||||
| @ -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) | ||||
|         ) | ||||
|  | ||||
| @ -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): | ||||
| @ -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: <pre>%(link)s</pre>" % {"link": link}) | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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", | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
|  | ||||
| @ -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",), | ||||
|     ] | ||||
| @ -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"), | ||||
|                 ] | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -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), | ||||
|         ), | ||||
|     ] | ||||
| @ -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): | ||||
|  | ||||
| @ -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): | ||||
							
								
								
									
										11
									
								
								passbook/channels/in_ldap/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								passbook/channels/in_ldap/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -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" | ||||
| @ -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 | ||||
| @ -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" | ||||
| @ -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", | ||||
| @ -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",), | ||||
|         ), | ||||
|     ] | ||||
| @ -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}" | ||||
| @ -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 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										33
									
								
								passbook/channels/in_ldap/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								passbook/channels/in_ldap/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||
							
								
								
									
										29
									
								
								passbook/channels/in_oauth/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								passbook/channels/in_oauth/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| @ -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): | ||||
							
								
								
									
										24
									
								
								passbook/channels/in_oauth/backends.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								passbook/channels/in_oauth/backends.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| @ -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) | ||||
| @ -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", | ||||
							
								
								
									
										81
									
								
								passbook/channels/in_oauth/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								passbook/channels/in_oauth/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -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",), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										159
									
								
								passbook/channels/in_oauth/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								passbook/channels/in_oauth/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -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: <pre>{url}</pre>" | ||||
|  | ||||
|     @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") | ||||
							
								
								
									
										15
									
								
								passbook/channels/in_oauth/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								passbook/channels/in_oauth/settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -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", | ||||
| ] | ||||
| @ -9,12 +9,12 @@ | ||||
| <div class="pf-c-card__body"> | ||||
|     {% if connections.exists %} | ||||
|     <p>{% trans 'Connected.' %}</p> | ||||
|     <a class="pf-c-button pf-m-danger" href="{% url 'passbook_sources_oauth:oauth-client-disconnect' source_slug=source.slug %}"> | ||||
|     <a class="pf-c-button pf-m-danger" href="{% url 'passbook_channels_in_oauth:oauth-client-disconnect' source_slug=source.slug %}"> | ||||
|         {% trans 'Disconnect' %} | ||||
|     </a> | ||||
|     {% else %} | ||||
|     <p>Not connected.</p> | ||||
|     <a class="pf-c-button pf-m-primary" href="{% url 'passbook_sources_oauth:oauth-client-login' source_slug=source.slug %}"> | ||||
|     <a class="pf-c-button pf-m-primary" href="{% url 'passbook_channels_in_oauth:oauth-client-login' source_slug=source.slug %}"> | ||||
|         {% trans 'Connect' %} | ||||
|     </a> | ||||
|     {% endif %} | ||||
| @ -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], | ||||
| @ -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"), | ||||
| @ -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", ""), | ||||
| @ -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", ""), | ||||
| @ -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", ""), | ||||
| @ -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() | ||||
| @ -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, | ||||
| @ -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", ""), | ||||
| @ -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/<slug:source_slug>/", | ||||
|         "login/<slug:inlet_slug>/", | ||||
|         dispatcher.DispatcherView.as_view(kind=RequestKind.redirect), | ||||
|         name="oauth-client-login", | ||||
|     ), | ||||
|     path( | ||||
|         "callback/<slug:source_slug>/", | ||||
|         "callback/<slug:inlet_slug>/", | ||||
|         dispatcher.DispatcherView.as_view(kind=RequestKind.callback), | ||||
|         name="oauth-client-callback", | ||||
|     ), | ||||
|     path( | ||||
|         "disconnect/<slug:source_slug>/", | ||||
|         "disconnect/<slug:inlet_slug>/", | ||||
|         core.DisconnectView.as_view(), | ||||
|         name="oauth-client-disconnect", | ||||
|     ), | ||||
|     path( | ||||
|         "user/<slug:source_slug>/", | ||||
|         "user/<slug:inlet_slug>/", | ||||
|         user.UserSettingsView.as_view(), | ||||
|         name="oauth-client-user", | ||||
|     ), | ||||
| @ -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,}, | ||||
|                 ), | ||||
|             }, | ||||
|         ) | ||||
| @ -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) | ||||
| @ -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) | ||||
							
								
								
									
										28
									
								
								passbook/channels/in_saml/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								passbook/channels/in_saml/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										12
									
								
								passbook/channels/in_saml/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								passbook/channels/in_saml/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -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/" | ||||
| @ -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", | ||||
							
								
								
									
										68
									
								
								passbook/channels/in_saml/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								passbook/channels/in_saml/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -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",), | ||||
|         ), | ||||
|     ] | ||||
| @ -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'<a href="{metadata_url}" class="btn btn-default btn-sm">Metadata Download</a>' | ||||
| 
 | ||||
|     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") | ||||
| @ -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]: | ||||
|         """ | ||||
| @ -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("<slug:source_slug>/", InitiateView.as_view(), name="login"), | ||||
							
								
								
									
										20
									
								
								passbook/channels/in_saml/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								passbook/channels/in_saml/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -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}) | ||||
|     ) | ||||
| @ -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, | ||||
|             }, | ||||
| @ -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() | ||||
| 
 | ||||
| @ -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 | ||||
| @ -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/" | ||||
| @ -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(), | ||||
| @ -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",), | ||||
|         ), | ||||
|     ] | ||||
| @ -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") | ||||
| @ -42,7 +42,7 @@ | ||||
|             <h1 class="pf-c-title pf-m-2xl">{% trans 'Setup with Kubernetes' %}</h1> | ||||
|             <div class="pf-c-modal-box__body"> | ||||
|                 <p>{% trans 'Download the manifest to create the Gatekeeper deployment and service:' %}</p> | ||||
|                 <a href="{% url 'passbook_providers_app_gw:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a> | ||||
|                 <a href="{% url 'passbook_channels_out_app_gw:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a> | ||||
|                 <p>{% trans 'Afterwards, add the following annotations to the Ingress you want to secure:' %}</p> | ||||
|                 <textarea class="codemirror" readonly data-cm-mode="yaml"> | ||||
| nginx.ingress.kubernetes.io/auth-url: "https://{{ provider.external_host }}/oauth2/auth" | ||||
| @ -1,7 +1,7 @@ | ||||
| """passbook app_gw urls""" | ||||
| from django.urls import path | ||||
| 
 | ||||
| from passbook.providers.app_gw.views import K8sManifestView | ||||
| from passbook.channels.out_app_gw.views import K8sManifestView | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     path( | ||||
| @ -9,7 +9,7 @@ from django.views import View | ||||
| from structlog import get_logger | ||||
| 
 | ||||
| from passbook import __version__ | ||||
| from passbook.providers.app_gw.models import ApplicationGatewayProvider | ||||
| from passbook.channels.out_app_gw.models import ApplicationGatewayOutlet | ||||
| 
 | ||||
| ORIGINAL_URL = "HTTP_X_ORIGINAL_URL" | ||||
| LOGGER = get_logger() | ||||
| @ -25,14 +25,14 @@ def get_cookie_secret(): | ||||
| class K8sManifestView(LoginRequiredMixin, View): | ||||
|     """Generate K8s Deployment and SVC for gatekeeper""" | ||||
| 
 | ||||
|     def get(self, request: HttpRequest, provider: int) -> HttpResponse: | ||||
|     def get(self, request: HttpRequest, outlet: int) -> HttpResponse: | ||||
|         """Render deployment template""" | ||||
|         provider = get_object_or_404(ApplicationGatewayProvider, pk=provider) | ||||
|         outlet = get_object_or_404(ApplicationGatewayOutlet, pk=outlet) | ||||
|         return render( | ||||
|             request, | ||||
|             "app_gw/k8s-manifest.yaml", | ||||
|             { | ||||
|                 "provider": provider, | ||||
|                 "outlet": outlet, | ||||
|                 "cookie_secret": get_cookie_secret(), | ||||
|                 "version": __version__, | ||||
|             }, | ||||
							
								
								
									
										29
									
								
								passbook/channels/out_oauth/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								passbook/channels/out_oauth/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| """OAuth2Outlet API Views""" | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from passbook.channels.out_oauth.models import OAuth2Outlet | ||||
|  | ||||
|  | ||||
| class OAuth2OutletSerializer(ModelSerializer): | ||||
|     """OAuth2Outlet Serializer""" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = OAuth2Outlet | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|             "redirect_uris", | ||||
|             "client_type", | ||||
|             "authorization_grant_type", | ||||
|             "client_id", | ||||
|             "client_secret", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class OAuth2OutletViewSet(ModelViewSet): | ||||
|     """OAuth2Outlet Viewset""" | ||||
|  | ||||
|     queryset = OAuth2Outlet.objects.all() | ||||
|     serializer_class = OAuth2OutletSerializer | ||||
							
								
								
									
										12
									
								
								passbook/channels/out_oauth/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								passbook/channels/out_oauth/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| """passbook auth oauth provider app config""" | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class PassbookOutletOAuthConfig(AppConfig): | ||||
|     """passbook auth oauth provider app config""" | ||||
|  | ||||
|     name = "passbook.channels.out_oauth" | ||||
|     label = "passbook_channels_out_oauth" | ||||
|     verbose_name = "passbook Outlets.OAuth" | ||||
|     mountpoint = "" | ||||
| @ -1,16 +1,16 @@ | ||||
| """passbook OAuth2 Provider Forms""" | ||||
| """passbook OAuth2 Outlet Forms""" | ||||
| 
 | ||||
| from django import forms | ||||
| 
 | ||||
| from passbook.providers.oauth.models import OAuth2Provider | ||||
| from passbook.channels.out_oauth.models import OAuth2Outlet | ||||
| 
 | ||||
| 
 | ||||
| class OAuth2ProviderForm(forms.ModelForm): | ||||
|     """OAuth2 Provider form""" | ||||
| class OAuth2OutletForm(forms.ModelForm): | ||||
|     """OAuth2 Outlet form""" | ||||
| 
 | ||||
|     class Meta: | ||||
| 
 | ||||
|         model = OAuth2Provider | ||||
|         model = OAuth2Outlet | ||||
|         fields = [ | ||||
|             "name", | ||||
|             "redirect_uris", | ||||
| @ -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:59 | ||||
| 
 | ||||
| import django.db.models.deletion | ||||
| import oauth2_provider.generators | ||||
| @ -16,22 +16,22 @@ class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|         ("passbook_core", "0001_initial"), | ||||
|         ("passbook_core", "__first__"), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="OAuth2Provider", | ||||
|             name="OAuth2Outlet", | ||||
|             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", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
| @ -90,15 +90,15 @@ class Migration(migrations.Migration): | ||||
|                         blank=True, | ||||
|                         null=True, | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         related_name="passbook_providers_oauth_oauth2provider", | ||||
|                         related_name="passbook_channels_out_oauth_oauth2outlet", | ||||
|                         to=settings.AUTH_USER_MODEL, | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "OAuth2 Provider", | ||||
|                 "verbose_name_plural": "OAuth2 Providers", | ||||
|                 "verbose_name": "OAuth2 Outlet", | ||||
|                 "verbose_name_plural": "OAuth2 Outlets", | ||||
|             }, | ||||
|             bases=("passbook_core.provider", models.Model), | ||||
|             bases=("passbook_core.outlet", models.Model), | ||||
|         ), | ||||
|     ] | ||||
| @ -7,17 +7,17 @@ from django.shortcuts import reverse | ||||
| from django.utils.translation import gettext as _ | ||||
| from oauth2_provider.models import AbstractApplication | ||||
| 
 | ||||
| from passbook.core.models import Provider | ||||
| from passbook.core.models import Outlet | ||||
| from passbook.lib.utils.template import render_to_string | ||||
| 
 | ||||
| 
 | ||||
| class OAuth2Provider(Provider, AbstractApplication): | ||||
| class OAuth2Outlet(Outlet, AbstractApplication): | ||||
|     """Associate an OAuth2 Application with a Product""" | ||||
| 
 | ||||
|     form = "passbook.providers.oauth.forms.OAuth2ProviderForm" | ||||
|     form = "passbook.channels.out_oauth.forms.OAuth2OutletForm" | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return f"OAuth2 Provider {self.name}" | ||||
|         return f"OAuth2 Outlet {self.name}" | ||||
| 
 | ||||
|     def html_setup_urls(self, request: HttpRequest) -> Optional[str]: | ||||
|         """return template and context modal with URLs for authorize, token, openid-config, etc""" | ||||
| @ -26,10 +26,10 @@ class OAuth2Provider(Provider, AbstractApplication): | ||||
|             { | ||||
|                 "provider": self, | ||||
|                 "authorize_url": request.build_absolute_uri( | ||||
|                     reverse("passbook_providers_oauth:oauth2-authorize") | ||||
|                     reverse("passbook_channels_out_oauth:oauth2-authorize") | ||||
|                 ), | ||||
|                 "token_url": request.build_absolute_uri( | ||||
|                     reverse("passbook_providers_oauth:token") | ||||
|                     reverse("passbook_channels_out_oauth:token") | ||||
|                 ), | ||||
|                 "userinfo_url": request.build_absolute_uri( | ||||
|                     reverse("passbook_api:openid") | ||||
| @ -39,5 +39,5 @@ class OAuth2Provider(Provider, AbstractApplication): | ||||
| 
 | ||||
|     class Meta: | ||||
| 
 | ||||
|         verbose_name = _("OAuth2 Provider") | ||||
|         verbose_name_plural = _("OAuth2 Providers") | ||||
|         verbose_name = _("OAuth2 Outlet") | ||||
|         verbose_name_plural = _("OAuth2 Outlets") | ||||
| @ -1,4 +1,4 @@ | ||||
| """passbook OAuth_Provider""" | ||||
| """passbook OAuth_Outlet""" | ||||
| from django.conf import settings | ||||
| 
 | ||||
| CORS_ORIGIN_ALLOW_ALL = settings.DEBUG | ||||
| @ -17,7 +17,7 @@ AUTHENTICATION_BACKENDS = [ | ||||
|     "oauth2_provider.backends.OAuth2Backend", | ||||
| ] | ||||
| 
 | ||||
| OAUTH2_PROVIDER_APPLICATION_MODEL = "passbook_providers_oauth.OAuth2Provider" | ||||
| OAUTH2_PROVIDER_APPLICATION_MODEL = "passbook_channels_out_oauth.OAuth2Outlet" | ||||
| 
 | ||||
| OAUTH2_PROVIDER = { | ||||
|     # this is the list of available scopes | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer