*: providers and sources -> channels, PolicyModel to PolicyBindingModel that uses custom M2M through

This commit is contained in:
Jens Langhammer 2020-05-15 22:15:01 +02:00
parent 615cd7870d
commit 7ed3ceb960
293 changed files with 3236 additions and 4684 deletions

View File

@ -0,0 +1,4 @@
"""passbook core inlet form fields"""
INLET_FORM_FIELDS = ["name", "slug", "enabled"]
INLET_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"]

View File

@ -1,4 +0,0 @@
"""passbook core source form fields"""
SOURCE_FORM_FIELDS = ["name", "slug", "enabled"]
SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"]

View File

@ -8,12 +8,12 @@ from passbook.admin.views import (
debug, debug,
flows, flows,
groups, groups,
inlets,
invitations, invitations,
outlets,
overview, overview,
policy, policies,
property_mapping, property_mapping,
providers,
sources,
stages, stages,
users, users,
) )
@ -39,51 +39,49 @@ urlpatterns = [
applications.ApplicationDeleteView.as_view(), applications.ApplicationDeleteView.as_view(),
name="application-delete", name="application-delete",
), ),
# Sources # Inlets
path("sources/", sources.SourceListView.as_view(), name="sources"), path("inlets/", inlets.InletListView.as_view(), name="inlets"),
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"), path("inlets/create/", inlets.InletCreateView.as_view(), name="inlet-create"),
path( path(
"sources/<uuid:pk>/update/", "inlets/<uuid:pk>/update/",
sources.SourceUpdateView.as_view(), inlets.InletUpdateView.as_view(),
name="source-update", name="inlet-update",
), ),
path( path(
"sources/<uuid:pk>/delete/", "inlets/<uuid:pk>/delete/",
sources.SourceDeleteView.as_view(), inlets.InletDeleteView.as_view(),
name="source-delete", name="inlet-delete",
), ),
# Policies # Policies
path("policies/", policy.PolicyListView.as_view(), name="policies"), path("policies/", policies.PolicyListView.as_view(), name="policies"),
path("policies/create/", policy.PolicyCreateView.as_view(), name="policy-create"), path("policies/create/", policies.PolicyCreateView.as_view(), name="policy-create"),
path( path(
"policies/<uuid:pk>/update/", "policies/<uuid:pk>/update/",
policy.PolicyUpdateView.as_view(), policies.PolicyUpdateView.as_view(),
name="policy-update", name="policy-update",
), ),
path( path(
"policies/<uuid:pk>/delete/", "policies/<uuid:pk>/delete/",
policy.PolicyDeleteView.as_view(), policies.PolicyDeleteView.as_view(),
name="policy-delete", name="policy-delete",
), ),
path( path(
"policies/<uuid:pk>/test/", policy.PolicyTestView.as_view(), name="policy-test" "policies/<uuid:pk>/test/",
policies.PolicyTestView.as_view(),
name="policy-test",
), ),
# Providers # Outlets
path("providers/", providers.ProviderListView.as_view(), name="providers"), path("outlets/", outlets.OutletListView.as_view(), name="outlets"),
path("outlets/create/", outlets.OutletCreateView.as_view(), name="outlet-create",),
path( path(
"providers/create/", "outlets/<int:pk>/update/",
providers.ProviderCreateView.as_view(), outlets.OutletUpdateView.as_view(),
name="provider-create", name="outlet-update",
), ),
path( path(
"providers/<int:pk>/update/", "outlets/<int:pk>/delete/",
providers.ProviderUpdateView.as_view(), outlets.OutletDeleteView.as_view(),
name="provider-update", name="outlet-delete",
),
path(
"providers/<int:pk>/delete/",
providers.ProviderDeleteView.as_view(),
name="provider-delete",
), ),
# Stages # Stages
path("stages/", stages.StageListView.as_view(), name="stages"), path("stages/", stages.StageListView.as_view(), name="stages"),

View File

@ -1,4 +1,4 @@
"""passbook Provider administration""" """passbook Inlet administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import ( 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 django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin 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.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView from passbook.lib.views import CreateAssignPermView
class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView): class InletListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all providers""" """Show list of all inlets"""
model = Provider model = Inlet
permission_required = "passbook_core.add_provider" permission_required = "passbook_core.view_inlet"
template_name = "administration/provider/list.html" ordering = "name"
paginate_by = 10 paginate_by = 40
ordering = "id" template_name = "administration/inlet/list.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs["types"] = { 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) return super().get_context_data(**kwargs)
@ -35,40 +35,40 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
return super().get_queryset().select_subclasses() return super().get_queryset().select_subclasses()
class ProviderCreateView( class InletCreateView(
SuccessMessageMixin, SuccessMessageMixin,
LoginRequiredMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, DjangoPermissionRequiredMixin,
CreateAssignPermView, CreateAssignPermView,
): ):
"""Create new Provider""" """Create new Inlet"""
model = Provider model = Inlet
permission_required = "passbook_core.add_provider" permission_required = "passbook_core.add_inlet"
template_name = "generic/create.html" template_name = "generic/create.html"
success_url = reverse_lazy("passbook_admin:providers") success_url = reverse_lazy("passbook_admin:inlets")
success_message = _("Successfully created Provider") success_message = _("Successfully created Inlet")
def get_form_class(self): def get_form_class(self):
provider_type = self.request.GET.get("type") inlet_type = self.request.GET.get("type")
model = next(x for x in all_subclasses(Provider) if x.__name__ == provider_type) model = next(x for x in all_subclasses(Inlet) if x.__name__ == inlet_type)
if not model: if not model:
raise Http404 raise Http404
return path_to_class(model.form) return path_to_class(model.form)
class ProviderUpdateView( class InletUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
): ):
"""Update provider""" """Update inlet"""
model = Provider model = Inlet
permission_required = "passbook_core.change_provider" permission_required = "passbook_core.change_inlet"
template_name = "generic/update.html" template_name = "generic/update.html"
success_url = reverse_lazy("passbook_admin:providers") success_url = reverse_lazy("passbook_admin:inlets")
success_message = _("Successfully updated Provider") success_message = _("Successfully updated Inlet")
def get_form_class(self): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
@ -77,29 +77,25 @@ class ProviderUpdateView(
def get_object(self, queryset=None): def get_object(self, queryset=None):
return ( return (
Provider.objects.filter(pk=self.kwargs.get("pk")) Inlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
.select_subclasses()
.first()
) )
class ProviderDeleteView( class InletDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
): ):
"""Delete provider""" """Delete inlet"""
model = Provider model = Inlet
permission_required = "passbook_core.delete_provider" permission_required = "passbook_core.delete_inlet"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:providers") success_url = reverse_lazy("passbook_admin:inlets")
success_message = _("Successfully deleted Provider") success_message = _("Successfully deleted Inlet")
def get_object(self, queryset=None): def get_object(self, queryset=None):
return ( return (
Provider.objects.filter(pk=self.kwargs.get("pk")) Inlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
.select_subclasses()
.first()
) )
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):

View File

@ -1,4 +1,4 @@
"""passbook Source administration""" """passbook Outlet administration"""
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import ( 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 django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin 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.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView from passbook.lib.views import CreateAssignPermView
class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView): class OutletListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all sources""" """Show list of all outlets"""
model = Source model = Outlet
permission_required = "passbook_core.view_source" permission_required = "passbook_core.add_outlet"
ordering = "name" template_name = "administration/outlet/list.html"
paginate_by = 40 paginate_by = 10
template_name = "administration/source/list.html" ordering = "id"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs["types"] = { 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) return super().get_context_data(**kwargs)
@ -35,40 +35,40 @@ class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
return super().get_queryset().select_subclasses() return super().get_queryset().select_subclasses()
class SourceCreateView( class OutletCreateView(
SuccessMessageMixin, SuccessMessageMixin,
LoginRequiredMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, DjangoPermissionRequiredMixin,
CreateAssignPermView, CreateAssignPermView,
): ):
"""Create new Source""" """Create new Outlet"""
model = Source model = Outlet
permission_required = "passbook_core.add_source" permission_required = "passbook_core.add_outlet"
template_name = "generic/create.html" template_name = "generic/create.html"
success_url = reverse_lazy("passbook_admin:sources") success_url = reverse_lazy("passbook_admin:outlets")
success_message = _("Successfully created Source") success_message = _("Successfully created Outlet")
def get_form_class(self): def get_form_class(self):
source_type = self.request.GET.get("type") outlet_type = self.request.GET.get("type")
model = next(x for x in all_subclasses(Source) if x.__name__ == source_type) model = next(x for x in all_subclasses(Outlet) if x.__name__ == outlet_type)
if not model: if not model:
raise Http404 raise Http404
return path_to_class(model.form) return path_to_class(model.form)
class SourceUpdateView( class OutletUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
): ):
"""Update source""" """Update outlet"""
model = Source model = Outlet
permission_required = "passbook_core.change_source" permission_required = "passbook_core.change_outlet"
template_name = "generic/update.html" template_name = "generic/update.html"
success_url = reverse_lazy("passbook_admin:sources") success_url = reverse_lazy("passbook_admin:outlets")
success_message = _("Successfully updated Source") success_message = _("Successfully updated Outlet")
def get_form_class(self): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
@ -77,25 +77,25 @@ class SourceUpdateView(
def get_object(self, queryset=None): def get_object(self, queryset=None):
return ( 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 SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
): ):
"""Delete source""" """Delete outlet"""
model = Source model = Outlet
permission_required = "passbook_core.delete_source" permission_required = "passbook_core.delete_outlet"
template_name = "generic/delete.html" template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:sources") success_url = reverse_lazy("passbook_admin:outlets")
success_message = _("Successfully deleted Source") success_message = _("Successfully deleted Outlet")
def get_object(self, queryset=None): def get_object(self, queryset=None):
return ( 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): def delete(self, request, *args, **kwargs):

View File

@ -5,8 +5,9 @@ from django.views.generic import TemplateView
from passbook import __version__ from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin 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.flows.models import Flow, Stage
from passbook.policies.models import Policy
from passbook.root.celery import CELERY_APP from passbook.root.celery import CELERY_APP
from passbook.stages.invitation.models import Invitation from passbook.stages.invitation.models import Invitation
@ -27,16 +28,14 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
kwargs["application_count"] = len(Application.objects.all()) kwargs["application_count"] = len(Application.objects.all())
kwargs["policy_count"] = len(Policy.objects.all()) kwargs["policy_count"] = len(Policy.objects.all())
kwargs["user_count"] = len(User.objects.all()) kwargs["user_count"] = len(User.objects.all())
kwargs["provider_count"] = len(Provider.objects.all()) kwargs["outlet_count"] = len(Outlet.objects.all())
kwargs["source_count"] = len(Source.objects.all()) kwargs["inlet_count"] = len(Inlet.objects.all())
kwargs["stage_count"] = len(Stage.objects.all()) kwargs["stage_count"] = len(Stage.objects.all())
kwargs["flow_count"] = len(Flow.objects.all()) kwargs["flow_count"] = len(Flow.objects.all())
kwargs["invitation_count"] = len(Invitation.objects.all()) kwargs["invitation_count"] = len(Invitation.objects.all())
kwargs["version"] = __version__ kwargs["version"] = __version__
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5)) kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
kwargs["providers_without_application"] = Provider.objects.filter( kwargs["outlets_without_application"] = Outlet.objects.filter(application=None)
application=None
)
kwargs["policies_without_binding"] = len( kwargs["policies_without_binding"] = len(
Policy.objects.filter(policymodel__isnull=True) Policy.objects.filter(policymodel__isnull=True)
) )

View File

@ -13,10 +13,10 @@ from django.views.generic.detail import DetailView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.forms.policies import PolicyTestForm 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.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView from passbook.lib.views import CreateAssignPermView
from passbook.policies.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
from passbook.policies.models import Policy
class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView): class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):

View File

@ -16,7 +16,7 @@ from guardian.mixins import (
) )
from passbook.admin.forms.users import UserForm 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 from passbook.lib.views import CreateAssignPermView
@ -92,12 +92,12 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
permission_required = "passbook_core.reset_user_password" permission_required = "passbook_core.reset_user_password"
def get(self, request, *args, **kwargs): 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) super().get(request, *args, **kwargs)
# TODO: create plan for user, get token # 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( 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( messages.success(
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link}) request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})

View File

@ -1,8 +1,8 @@
"""permission classes for django restframework""" """permission classes for django restframework"""
from rest_framework.permissions import BasePermission, DjangoObjectPermissions from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from passbook.core.models import PolicyModel
from passbook.policies.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
from passbook.policies.models import PolicyBindingModel
class CustomObjectPermissions(DjangoObjectPermissions): class CustomObjectPermissions(DjangoObjectPermissions):
@ -24,8 +24,7 @@ class PolicyPermissions(BasePermission):
policy_engine: PolicyEngine policy_engine: PolicyEngine
def has_object_permission(self, request, view, obj: PolicyModel) -> bool: def has_object_permission(self, request, view, obj: PolicyBindingModel) -> bool:
# if not obj.po self.policy_engine = PolicyEngine(obj.policies.all(), request.user, request)
self.policy_engine = PolicyEngine(obj.policies, request.user, request)
self.policy_engine.request.obj = obj self.policy_engine.request.obj = obj
return self.policy_engine.build().passing return self.policy_engine.build().passing

View File

@ -9,12 +9,18 @@ from structlog import get_logger
from passbook.api.permissions import CustomObjectPermissions from passbook.api.permissions import CustomObjectPermissions
from passbook.audit.api import EventViewSet 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.applications import ApplicationViewSet
from passbook.core.api.groups import GroupViewSet 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.policies import PolicyViewSet
from passbook.core.api.propertymappings import PropertyMappingViewSet 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.core.api.users import UserViewSet
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
from passbook.lib.utils.reflection import get_apps 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.hibp.api import HaveIBeenPwendPolicyViewSet
from passbook.policies.password.api import PasswordPolicyViewSet from passbook.policies.password.api import PasswordPolicyViewSet
from passbook.policies.reputation.api import ReputationPolicyViewSet 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.captcha.api import CaptchaStageViewSet
from passbook.stages.email.api import EmailStageViewSet from passbook.stages.email.api import EmailStageViewSet
from passbook.stages.identification.api import IdentificationStageViewSet from passbook.stages.identification.api import IdentificationStageViewSet
@ -57,9 +57,15 @@ router.register("core/users", UserViewSet)
router.register("audit/events", EventViewSet) router.register("audit/events", EventViewSet)
router.register("sources/all", SourceViewSet) router.register("inlets/all", InletViewSet)
router.register("sources/ldap", LDAPSourceViewSet) router.register("inlets/ldap", LDAPInletViewSet)
router.register("sources/oauth", OAuthSourceViewSet) 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/all", PolicyViewSet)
router.register("policies/bindings", PolicyBindingViewSet) router.register("policies/bindings", PolicyBindingViewSet)
@ -69,12 +75,6 @@ router.register("policies/password", PasswordPolicyViewSet)
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet) router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
router.register("policies/reputation", ReputationPolicyViewSet) 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/all", PropertyMappingViewSet)
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet) router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
router.register("propertymappings/saml", SAMLPropertyMappingViewSet) router.register("propertymappings/saml", SAMLPropertyMappingViewSet)

View File

@ -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 import uuid
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="AuditEntry", name="Event",
fields=[ fields=[
( (
"uuid", "uuid",
@ -33,15 +33,16 @@ class Migration(migrations.Migration):
"action", "action",
models.TextField( models.TextField(
choices=[ choices=[
("login", "login"), ("LOGIN", "login"),
("login_failed", "login_failed"), ("LOGIN_FAILED", "login_failed"),
("logout", "logout"), ("LOGOUT", "logout"),
("authorize_application", "authorize_application"), ("AUTHORIZE_APPLICATION", "authorize_application"),
("suspicious_request", "suspicious_request"), ("SUSPICIOUS_REQUEST", "suspicious_request"),
("sign_up", "sign_up"), ("SIGN_UP", "sign_up"),
("password_reset", "password_reset"), ("PASSWORD_RESET", "password_reset"),
("invitation_created", "invitation_created"), ("INVITE_CREATED", "invitation_created"),
("invitation_used", "invitation_used"), ("INVITE_USED", "invitation_used"),
("CUSTOM", "custom"),
] ]
), ),
), ),
@ -53,7 +54,7 @@ class Migration(migrations.Migration):
blank=True, default=dict blank=True, default=dict
), ),
), ),
("request_ip", models.GenericIPAddressField()), ("client_ip", models.GenericIPAddressField(null=True)),
("created", models.DateTimeField(auto_now_add=True)), ("created", models.DateTimeField(auto_now_add=True)),
( (
"user", "user",
@ -65,8 +66,8 @@ class Migration(migrations.Migration):
), ),
], ],
options={ options={
"verbose_name": "Audit Entry", "verbose_name": "Audit Event",
"verbose_name_plural": "Audit Entries", "verbose_name_plural": "Audit Events",
}, },
), ),
] ]

View File

@ -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",),
]

View File

@ -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"),
]
),
),
]

View File

@ -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),
),
]

View File

@ -5,7 +5,7 @@ from django.test import TestCase
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from passbook.audit.models import Event, EventAction from passbook.audit.models import Event, EventAction
from passbook.core.models import Policy from passbook.policies.models import Policy
class TestAuditEvent(TestCase): class TestAuditEvent(TestCase):

View File

@ -1,17 +1,17 @@
"""Source API Views""" """Inlet API Views"""
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from passbook.admin.forms.source import SOURCE_SERIALIZER_FIELDS from passbook.admin.forms.inlet import INLET_SERIALIZER_FIELDS
from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource from passbook.channels.in_ldap.models import LDAPInlet, LDAPPropertyMapping
class LDAPSourceSerializer(ModelSerializer): class LDAPInletSerializer(ModelSerializer):
"""LDAP Source Serializer""" """LDAP Inlet Serializer"""
class Meta: class Meta:
model = LDAPSource model = LDAPInlet
fields = SOURCE_SERIALIZER_FIELDS + [ fields = INLET_SERIALIZER_FIELDS + [
"server_uri", "server_uri",
"bind_cn", "bind_cn",
"bind_password", "bind_password",
@ -38,11 +38,11 @@ class LDAPPropertyMappingSerializer(ModelSerializer):
fields = ["pk", "name", "expression", "object_field"] fields = ["pk", "name", "expression", "object_field"]
class LDAPSourceViewSet(ModelViewSet): class LDAPInletViewSet(ModelViewSet):
"""LDAP Source Viewset""" """LDAP Inlet Viewset"""
queryset = LDAPSource.objects.all() queryset = LDAPInlet.objects.all()
serializer_class = LDAPSourceSerializer serializer_class = LDAPInletSerializer
class LDAPPropertyMappingViewSet(ModelViewSet): class LDAPPropertyMappingViewSet(ModelViewSet):

View 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"

View File

@ -3,8 +3,8 @@ from django.contrib.auth.backends import ModelBackend
from django.http import HttpRequest from django.http import HttpRequest
from structlog import get_logger from structlog import get_logger
from passbook.sources.ldap.connector import Connector from passbook.channels.in_ldap.connector import Connector
from passbook.sources.ldap.models import LDAPSource from passbook.channels.in_ldap.models import LDAPInlet
LOGGER = get_logger() LOGGER = get_logger()
@ -16,9 +16,9 @@ class LDAPBackend(ModelBackend):
"""Try to authenticate a user via ldap""" """Try to authenticate a user via ldap"""
if "password" not in kwargs: if "password" not in kwargs:
return None return None
for source in LDAPSource.objects.filter(enabled=True): for inlet in LDAPInlet.objects.filter(enabled=True):
LOGGER.debug("LDAP Auth attempt", source=source) LOGGER.debug("LDAP Auth attempt", inlet=inlet)
_ldap = Connector(source) _ldap = Connector(inlet)
user = _ldap.auth_user(**kwargs) user = _ldap.auth_user(**kwargs)
if user: if user:
return user return user

View File

@ -6,9 +6,9 @@ import ldap3.core.exceptions
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from structlog import get_logger from structlog import get_logger
from passbook.channels.in_ldap.models import LDAPInlet, LDAPPropertyMapping
from passbook.core.exceptions import PropertyMappingExpressionException from passbook.core.exceptions import PropertyMappingExpressionException
from passbook.core.models import Group, User from passbook.core.models import Group, User
from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource
LOGGER = get_logger() LOGGER = get_logger()
@ -18,23 +18,23 @@ class Connector:
_server: ldap3.Server _server: ldap3.Server
_connection = ldap3.Connection _connection = ldap3.Connection
_source: LDAPSource _inlet: LDAPInlet
def __init__(self, source: LDAPSource): def __init__(self, source: LDAPInlet):
self._source = source self._inlet = source
self._server = ldap3.Server(source.server_uri) # Implement URI parsing self._server = ldap3.Server(source.server_uri) # Implement URI parsing
def bind(self): def bind(self):
"""Bind using Source's Credentials""" """Bind using Inlet's Credentials"""
self._connection = ldap3.Connection( self._connection = ldap3.Connection(
self._server, self._server,
raise_exceptions=True, raise_exceptions=True,
user=self._source.bind_cn, user=self._inlet.bind_cn,
password=self._source.bind_password, password=self._inlet.bind_password,
) )
self._connection.bind() self._connection.bind()
if self._source.start_tls: if self._inlet.start_tls:
self._connection.start_tls() self._connection.start_tls()
@staticmethod @staticmethod
@ -45,21 +45,21 @@ class Connector:
@property @property
def base_dn_users(self) -> str: def base_dn_users(self) -> str:
"""Shortcut to get full base_dn for user lookups""" """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 @property
def base_dn_groups(self) -> str: def base_dn_groups(self) -> str:
"""Shortcut to get full base_dn for group lookups""" """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): def sync_groups(self):
"""Iterate over all LDAP Groups and create passbook_core.Group instances""" """Iterate over all LDAP Groups and create passbook_core.Group instances"""
if not self._source.sync_groups: if not self._inlet.sync_groups:
LOGGER.debug("Group syncing is disabled for this Source") LOGGER.debug("Group syncing is disabled for this Inlet")
return return
groups = self._connection.extend.standard.paged_search( groups = self._connection.extend.standard.paged_search(
search_base=self.base_dn_groups, search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter, search_filter=self._inlet.group_object_filter,
search_scope=ldap3.SUBTREE, search_scope=ldap3.SUBTREE,
attributes=ldap3.ALL_ATTRIBUTES, attributes=ldap3.ALL_ATTRIBUTES,
) )
@ -67,15 +67,15 @@ class Connector:
attributes = group.get("attributes", {}) attributes = group.get("attributes", {})
_, created = Group.objects.update_or_create( _, created = Group.objects.update_or_create(
attributes__ldap_uniq=attributes.get( 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=self._build_object_properties(attributes),
defaults={ defaults={
"name": attributes.get("name", ""), "name": attributes.get("name", ""),
"attributes": { "attributes": {
"ldap_uniq": attributes.get( "ldap_uniq": attributes.get(
self._source.object_uniqueness_field, "" self._inlet.object_uniqueness_field, ""
), ),
"distinguishedName": attributes.get("distinguishedName"), "distinguishedName": attributes.get("distinguishedName"),
}, },
@ -89,14 +89,14 @@ class Connector:
"""Iterate over all LDAP Users and create passbook_core.User instances""" """Iterate over all LDAP Users and create passbook_core.User instances"""
users = self._connection.extend.standard.paged_search( users = self._connection.extend.standard.paged_search(
search_base=self.base_dn_users, search_base=self.base_dn_users,
search_filter=self._source.user_object_filter, search_filter=self._inlet.user_object_filter,
search_scope=ldap3.SUBTREE, search_scope=ldap3.SUBTREE,
attributes=ldap3.ALL_ATTRIBUTES, attributes=ldap3.ALL_ATTRIBUTES,
) )
for user in users: for user in users:
attributes = user.get("attributes", {}) attributes = user.get("attributes", {})
try: try:
uniq = attributes[self._source.object_uniqueness_field] uniq = attributes[self._inlet.object_uniqueness_field]
except KeyError: except KeyError:
LOGGER.warning("Cannot find uniqueness Field in attributes") LOGGER.warning("Cannot find uniqueness Field in attributes")
continue continue
@ -125,20 +125,20 @@ class Connector:
"""Iterate over all Users and assign Groups using memberOf Field""" """Iterate over all Users and assign Groups using memberOf Field"""
users = self._connection.extend.standard.paged_search( users = self._connection.extend.standard.paged_search(
search_base=self.base_dn_users, search_base=self.base_dn_users,
search_filter=self._source.user_object_filter, search_filter=self._inlet.user_object_filter,
search_scope=ldap3.SUBTREE, search_scope=ldap3.SUBTREE,
attributes=[ attributes=[
self._source.user_group_membership_field, self._inlet.user_group_membership_field,
self._source.object_uniqueness_field, self._inlet.object_uniqueness_field,
], ],
) )
group_cache: Dict[str, Group] = {} group_cache: Dict[str, Group] = {}
for user in users: for user in users:
member_of = user.get("attributes", {}).get( member_of = user.get("attributes", {}).get(
self._source.user_group_membership_field, [] self._inlet.user_group_membership_field, []
) )
uniq = user.get("attributes", {}).get( uniq = user.get("attributes", {}).get(
self._source.object_uniqueness_field, [] self._inlet.object_uniqueness_field, []
) )
for group_dn in member_of: for group_dn in member_of:
# Check if group_dn is within our base_dn_groups, and skip if not # 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] self, attributes: Dict[str, Any]
) -> Dict[str, Dict[Any, Any]]: ) -> Dict[str, Dict[Any, Any]]:
properties = {"attributes": {}} 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): if not isinstance(mapping, LDAPPropertyMapping):
continue continue
mapping: LDAPPropertyMapping mapping: LDAPPropertyMapping
@ -179,9 +179,9 @@ class Connector:
except PropertyMappingExpressionException as exc: except PropertyMappingExpressionException as exc:
LOGGER.warning("Mapping failed to evaluate", exc=exc, mapping=mapping) LOGGER.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
continue continue
if self._source.object_uniqueness_field in attributes: if self._inlet.object_uniqueness_field in attributes:
properties["attributes"]["ldap_uniq"] = attributes.get( properties["attributes"]["ldap_uniq"] = attributes.get(
self._source.object_uniqueness_field self._inlet.object_uniqueness_field
) )
properties["attributes"]["distinguishedName"] = attributes.get( properties["attributes"]["distinguishedName"] = attributes.get(
"distinguishedName" "distinguishedName"

View File

@ -4,17 +4,17 @@ from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passbook.admin.forms.source import SOURCE_FORM_FIELDS from passbook.admin.forms.inlet import INLET_FORM_FIELDS
from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource from passbook.channels.in_ldap.models import LDAPInlet, LDAPPropertyMapping
class LDAPSourceForm(forms.ModelForm): class LDAPInletForm(forms.ModelForm):
"""LDAPSource Form""" """LDAPInlet Form"""
class Meta: class Meta:
model = LDAPSource model = LDAPInlet
fields = SOURCE_FORM_FIELDS + [ fields = INLET_FORM_FIELDS + [
"server_uri", "server_uri",
"bind_cn", "bind_cn",
"bind_password", "bind_password",

View File

@ -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.core.validators
import django.db.models.deletion import django.db.models.deletion
@ -10,7 +10,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("passbook_core", "0001_initial"), ("passbook_core", "__first__"),
] ]
operations = [ operations = [
@ -28,69 +28,104 @@ class Migration(migrations.Migration):
to="passbook_core.PropertyMapping", to="passbook_core.PropertyMapping",
), ),
), ),
("ldap_property", models.TextField()),
("object_field", 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",), bases=("passbook_core.propertymapping",),
), ),
migrations.CreateModel( migrations.CreateModel(
name="LDAPSource", name="LDAPInlet",
fields=[ fields=[
( (
"source_ptr", "inlet_ptr",
models.OneToOneField( models.OneToOneField(
auto_created=True, auto_created=True,
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
parent_link=True, parent_link=True,
primary_key=True, primary_key=True,
serialize=False, serialize=False,
to="passbook_core.Source", to="passbook_core.Inlet",
), ),
), ),
( (
"server_uri", "server_uri",
models.URLField( models.TextField(
validators=[ validators=[
django.core.validators.URLValidator( django.core.validators.URLValidator(
schemes=["ldap", "ldaps"] schemes=["ldap", "ldaps"]
) )
] ],
verbose_name="Server URI",
), ),
), ),
("bind_cn", models.TextField()), ("bind_cn", models.TextField(verbose_name="Bind CN")),
("bind_password", models.TextField()), ("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", "additional_user_dn",
models.TextField( 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", "additional_group_dn",
models.TextField( 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_groups", models.BooleanField(default=True)),
( (
"sync_parent_group", "sync_parent_group",
models.ForeignKey( models.ForeignKey(
blank=True, blank=True,
default=None, default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT, on_delete=django.db.models.deletion.SET_DEFAULT,
to="passbook_core.Group", to="passbook_core.Group",
), ),
), ),
], ],
options={ options={
"verbose_name": "LDAP Source", "verbose_name": "LDAP Inlet",
"verbose_name_plural": "LDAP Sources", "verbose_name_plural": "LDAP Inlets",
}, },
bases=("passbook_core.source",), bases=("passbook_core.inlet",),
), ),
] ]

View File

@ -4,11 +4,11 @@ from django.core.validators import URLValidator
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ 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): class LDAPInlet(Inlet):
"""LDAP Authentication source""" """LDAP Authentication inlet"""
server_uri = models.TextField( server_uri = models.TextField(
validators=[URLValidator(schemes=["ldap", "ldaps"])], validators=[URLValidator(schemes=["ldap", "ldaps"])],
@ -48,12 +48,12 @@ class LDAPSource(Source):
Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT 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: class Meta:
verbose_name = _("LDAP Source") verbose_name = _("LDAP Inlet")
verbose_name_plural = _("LDAP Sources") verbose_name_plural = _("LDAP Inlets")
class LDAPPropertyMapping(PropertyMapping): class LDAPPropertyMapping(PropertyMapping):
@ -61,7 +61,7 @@ class LDAPPropertyMapping(PropertyMapping):
object_field = models.TextField() object_field = models.TextField()
form = "passbook.sources.ldap.forms.LDAPPropertyMappingForm" form = "passbook.channels.in_ldap.forms.LDAPPropertyMappingForm"
def __str__(self): def __str__(self):
return f"LDAP Property Mapping {self.expression} -> {self.object_field}" return f"LDAP Property Mapping {self.expression} -> {self.object_field}"

View File

@ -2,12 +2,12 @@
from celery.schedules import crontab from celery.schedules import crontab
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
"passbook.sources.ldap.auth.LDAPBackend", "passbook.channels.in_ldap.auth.LDAPBackend",
] ]
CELERY_BEAT_SCHEDULE = { CELERY_BEAT_SCHEDULE = {
"sync": { "sync": {
"task": "passbook.sources.ldap.tasks.sync", "task": "passbook.channels.in_ldap.tasks.sync",
"schedule": crontab(minute=0), # Run every hour "schedule": crontab(minute=0), # Run every hour
} }
} }

View 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()

View 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

View File

@ -8,12 +8,12 @@ from structlog import get_logger
LOGGER = get_logger() LOGGER = get_logger()
class PassbookSourceOAuthConfig(AppConfig): class PassbookInletOAuthConfig(AppConfig):
"""passbook source.oauth config""" """passbook source.oauth config"""
name = "passbook.sources.oauth" name = "passbook.channels.in_oauth"
label = "passbook_sources_oauth" label = "passbook_channels_in_oauth"
verbose_name = "passbook Sources.OAuth" verbose_name = "passbook Inlets.OAuth"
mountpoint = "source/oauth/" mountpoint = "source/oauth/"
def ready(self): def ready(self):

View 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

View File

@ -21,8 +21,8 @@ class BaseOAuthClient:
session: Session = None session: Session = None
def __init__(self, source, token=""): # nosec def __init__(self, inlet, token=""): # nosec
self.source = source self.inlet = inlet
self.token = token self.token = token
self.session = Session() self.session = Session()
self.session.headers.update({"User-Agent": "passbook %s" % __version__}) self.session.headers.update({"User-Agent": "passbook %s" % __version__})
@ -38,7 +38,7 @@ class BaseOAuthClient:
"Authorization": f"{token['token_type']} {token['access_token']}" "Authorization": f"{token['token_type']} {token['access_token']}"
} }
response = self.session.request( response = self.session.request(
"get", self.source.profile_url, headers=headers, "get", self.inlet.profile_url, headers=headers,
) )
response.raise_for_status() response.raise_for_status()
except RequestException as exc: except RequestException as exc:
@ -58,7 +58,7 @@ class BaseOAuthClient:
args.update(additional) args.update(additional)
params = urlencode(args) params = urlencode(args)
LOGGER.info("redirect args", **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): def parse_raw_token(self, raw_token):
"Parse token and secret from raw token response." "Parse token and secret from raw token response."
@ -94,7 +94,7 @@ class OAuthClient(BaseOAuthClient):
try: try:
response = self.session.request( response = self.session.request(
"post", "post",
self.source.access_token_url, self.inlet.access_token_url,
data=data, data=data,
headers=self._default_headers, headers=self._default_headers,
) )
@ -112,7 +112,7 @@ class OAuthClient(BaseOAuthClient):
try: try:
response = self.session.request( response = self.session.request(
"post", "post",
self.source.request_token_url, self.inlet.request_token_url,
data={"oauth_callback": callback}, data={"oauth_callback": callback},
headers=self._default_headers, headers=self._default_headers,
) )
@ -151,10 +151,10 @@ class OAuthClient(BaseOAuthClient):
callback = kwargs.pop("oauth_callback", None) callback = kwargs.pop("oauth_callback", None)
verifier = kwargs.get("data", {}).pop("oauth_verifier", None) verifier = kwargs.get("data", {}).pop("oauth_verifier", None)
oauth = OAuth1( oauth = OAuth1(
resource_owner_key=token, reinlet_owner_key=token,
resource_owner_secret=secret, reinlet_owner_secret=secret,
client_key=self.source.consumer_key, client_key=self.inlet.consumer_key,
client_secret=self.source.consumer_secret, client_secret=self.inlet.consumer_secret,
verifier=verifier, verifier=verifier,
callback_uri=callback, callback_uri=callback,
) )
@ -163,7 +163,7 @@ class OAuthClient(BaseOAuthClient):
@property @property
def session_key(self): 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): class OAuth2Client(BaseOAuthClient):
@ -183,7 +183,7 @@ class OAuth2Client(BaseOAuthClient):
if returned is not None: if returned is not None:
check = constant_time_compare(stored, returned) check = constant_time_compare(stored, returned)
else: else:
LOGGER.warning("No state parameter returned by the source.") LOGGER.warning("No state parameter returned by the inlet.")
else: else:
LOGGER.warning("No state stored in the sesssion.") LOGGER.warning("No state stored in the sesssion.")
return check return check
@ -196,19 +196,19 @@ class OAuth2Client(BaseOAuthClient):
return None return None
if "code" in request.GET: if "code" in request.GET:
args = { args = {
"client_id": self.source.consumer_key, "client_id": self.inlet.consumer_key,
"redirect_uri": callback, "redirect_uri": callback,
"client_secret": self.source.consumer_secret, "client_secret": self.inlet.consumer_secret,
"code": request.GET["code"], "code": request.GET["code"],
"grant_type": "authorization_code", "grant_type": "authorization_code",
} }
else: else:
LOGGER.warning("No code returned by the source") LOGGER.warning("No code returned by the inlet")
return None return None
try: try:
response = self.session.request( response = self.session.request(
"post", "post",
self.source.access_token_url, self.inlet.access_token_url,
data=args, data=args,
headers=self._default_headers, headers=self._default_headers,
**request_kwargs, **request_kwargs,
@ -229,7 +229,7 @@ class OAuth2Client(BaseOAuthClient):
"Get request parameters for redirect url." "Get request parameters for redirect url."
callback = request.build_absolute_uri(callback) callback = request.build_absolute_uri(callback)
args = { args = {
"client_id": self.source.consumer_key, "client_id": self.inlet.consumer_key,
"redirect_uri": callback, "redirect_uri": callback,
"response_type": "code", "response_type": "code",
} }
@ -264,12 +264,12 @@ class OAuth2Client(BaseOAuthClient):
@property @property
def session_key(self): 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 def get_client(inlet, token=""): # nosec
"Return the API client for the given source." "Return the API client for the given inlet."
cls = OAuth2Client cls = OAuth2Client
if source.request_token_url: if inlet.request_token_url:
cls = OAuthClient cls = OAuthClient
return cls(source, token) return cls(inlet, token)

View File

@ -2,13 +2,13 @@
from django import forms from django import forms
from passbook.admin.forms.source import SOURCE_FORM_FIELDS from passbook.admin.forms.inlet import INLET_FORM_FIELDS
from passbook.sources.oauth.models import OAuthSource from passbook.channels.in_oauth.models import OAuthInlet
from passbook.sources.oauth.types.manager import MANAGER from passbook.channels.in_oauth.types.manager import MANAGER
class OAuthSourceForm(forms.ModelForm): class OAuthInletForm(forms.ModelForm):
"""OAuthSource Form""" """OAuthInlet Form"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -19,8 +19,8 @@ class OAuthSourceForm(forms.ModelForm):
class Meta: class Meta:
model = OAuthSource model = OAuthInlet
fields = SOURCE_FORM_FIELDS + [ fields = INLET_FORM_FIELDS + [
"provider_type", "provider_type",
"request_token_url", "request_token_url",
"authorization_url", "authorization_url",
@ -37,10 +37,10 @@ class OAuthSourceForm(forms.ModelForm):
} }
class GitHubOAuthSourceForm(OAuthSourceForm): class GitHubOAuthInletForm(OAuthInletForm):
"""OAuth Source form with pre-determined URL for GitHub""" """OAuth Inlet form with pre-determined URL for GitHub"""
class Meta(OAuthSourceForm.Meta): class Meta(OAuthInletForm.Meta):
overrides = { overrides = {
"provider_type": "github", "provider_type": "github",
@ -51,10 +51,10 @@ class GitHubOAuthSourceForm(OAuthSourceForm):
} }
class TwitterOAuthSourceForm(OAuthSourceForm): class TwitterOAuthInletForm(OAuthInletForm):
"""OAuth Source form with pre-determined URL for Twitter""" """OAuth Inlet form with pre-determined URL for Twitter"""
class Meta(OAuthSourceForm.Meta): class Meta(OAuthInletForm.Meta):
overrides = { overrides = {
"provider_type": "twitter", "provider_type": "twitter",
@ -68,10 +68,10 @@ class TwitterOAuthSourceForm(OAuthSourceForm):
} }
class FacebookOAuthSourceForm(OAuthSourceForm): class FacebookOAuthInletForm(OAuthInletForm):
"""OAuth Source form with pre-determined URL for Facebook""" """OAuth Inlet form with pre-determined URL for Facebook"""
class Meta(OAuthSourceForm.Meta): class Meta(OAuthInletForm.Meta):
overrides = { overrides = {
"provider_type": "facebook", "provider_type": "facebook",
@ -82,10 +82,10 @@ class FacebookOAuthSourceForm(OAuthSourceForm):
} }
class DiscordOAuthSourceForm(OAuthSourceForm): class DiscordOAuthInletForm(OAuthInletForm):
"""OAuth Source form with pre-determined URL for Discord""" """OAuth Inlet form with pre-determined URL for Discord"""
class Meta(OAuthSourceForm.Meta): class Meta(OAuthInletForm.Meta):
overrides = { overrides = {
"provider_type": "discord", "provider_type": "discord",
@ -96,10 +96,10 @@ class DiscordOAuthSourceForm(OAuthSourceForm):
} }
class GoogleOAuthSourceForm(OAuthSourceForm): class GoogleOAuthInletForm(OAuthInletForm):
"""OAuth Source form with pre-determined URL for Google""" """OAuth Inlet form with pre-determined URL for Google"""
class Meta(OAuthSourceForm.Meta): class Meta(OAuthInletForm.Meta):
overrides = { overrides = {
"provider_type": "google", "provider_type": "google",
@ -110,10 +110,10 @@ class GoogleOAuthSourceForm(OAuthSourceForm):
} }
class AzureADOAuthSourceForm(OAuthSourceForm): class AzureADOAuthInletForm(OAuthInletForm):
"""OAuth Source form with pre-determined URL for AzureAD""" """OAuth Inlet form with pre-determined URL for AzureAD"""
class Meta(OAuthSourceForm.Meta): class Meta(OAuthInletForm.Meta):
overrides = { overrides = {
"provider_type": "azure-ad", "provider_type": "azure-ad",

View 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",),
),
]

View 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")

View 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",
]

View File

@ -9,12 +9,12 @@
<div class="pf-c-card__body"> <div class="pf-c-card__body">
{% if connections.exists %} {% if connections.exists %}
<p>{% trans 'Connected.' %}</p> <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' %} {% trans 'Disconnect' %}
</a> </a>
{% else %} {% else %}
<p>Not connected.</p> <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' %} {% trans 'Connect' %}
</a> </a>
{% endif %} {% endif %}

View File

@ -1,19 +1,19 @@
"""AzureAD OAuth2 Views""" """AzureAD OAuth2 Views"""
import uuid import uuid
from passbook.sources.oauth.types.manager import MANAGER, RequestKind from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
from passbook.sources.oauth.utils import user_get_or_create from passbook.channels.in_oauth.utils import user_get_or_create
from passbook.sources.oauth.views.core import OAuthCallback 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): class AzureADOAuthCallback(OAuthCallback):
"""AzureAD OAuth2 Callback""" """AzureAD OAuth2 Callback"""
def get_user_id(self, source, info): def get_user_id(self, inlet, info):
return uuid.UUID(info.get("objectId")).int 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 = { user_data = {
"username": info.get("displayName"), "username": info.get("displayName"),
"email": info.get("mail", None) or info.get("otherMails")[0], "email": info.get("mail", None) or info.get("otherMails")[0],

View File

@ -1,24 +1,24 @@
"""Discord OAuth Views""" """Discord OAuth Views"""
from passbook.sources.oauth.types.manager import MANAGER, RequestKind from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
from passbook.sources.oauth.utils import user_get_or_create from passbook.channels.in_oauth.utils import user_get_or_create
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect 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): class DiscordOAuthRedirect(OAuthRedirect):
"""Discord OAuth2 Redirect""" """Discord OAuth2 Redirect"""
def get_additional_parameters(self, source): def get_additional_parameters(self, inlet):
return { return {
"scope": "email identify", "scope": "email identify",
} }
@MANAGER.source(kind=RequestKind.callback, name="Discord") @MANAGER.inlet(kind=RequestKind.callback, name="Discord")
class DiscordOAuth2Callback(OAuthCallback): class DiscordOAuth2Callback(OAuthCallback):
"""Discord OAuth2 Callback""" """Discord OAuth2 Callback"""
def get_or_create_user(self, source, access, info): def get_or_create_user(self, inlet, access, info):
user_data = { user_data = {
"username": info.get("username"), "username": info.get("username"),
"email": info.get("email", "None"), "email": info.get("email", "None"),

View File

@ -1,24 +1,24 @@
"""Facebook OAuth Views""" """Facebook OAuth Views"""
from passbook.sources.oauth.types.manager import MANAGER, RequestKind from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
from passbook.sources.oauth.utils import user_get_or_create from passbook.channels.in_oauth.utils import user_get_or_create
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect 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): class FacebookOAuthRedirect(OAuthRedirect):
"""Facebook OAuth2 Redirect""" """Facebook OAuth2 Redirect"""
def get_additional_parameters(self, source): def get_additional_parameters(self, inlet):
return { return {
"scope": "email", "scope": "email",
} }
@MANAGER.source(kind=RequestKind.callback, name="Facebook") @MANAGER.inlet(kind=RequestKind.callback, name="Facebook")
class FacebookOAuth2Callback(OAuthCallback): class FacebookOAuth2Callback(OAuthCallback):
"""Facebook OAuth2 Callback""" """Facebook OAuth2 Callback"""
def get_or_create_user(self, source, access, info): def get_or_create_user(self, inlet, access, info):
user_data = { user_data = {
"username": info.get("name"), "username": info.get("name"),
"email": info.get("email", ""), "email": info.get("email", ""),

View File

@ -1,14 +1,14 @@
"""GitHub OAuth Views""" """GitHub OAuth Views"""
from passbook.sources.oauth.types.manager import MANAGER, RequestKind from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
from passbook.sources.oauth.utils import user_get_or_create from passbook.channels.in_oauth.utils import user_get_or_create
from passbook.sources.oauth.views.core import OAuthCallback 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): class GitHubOAuth2Callback(OAuthCallback):
"""GitHub OAuth2 Callback""" """GitHub OAuth2 Callback"""
def get_or_create_user(self, source, access, info): def get_or_create_user(self, inlet, access, info):
user_data = { user_data = {
"username": info.get("login"), "username": info.get("login"),
"email": info.get("email", ""), "email": info.get("email", ""),

View File

@ -1,24 +1,24 @@
"""Google OAuth Views""" """Google OAuth Views"""
from passbook.sources.oauth.types.manager import MANAGER, RequestKind from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
from passbook.sources.oauth.utils import user_get_or_create from passbook.channels.in_oauth.utils import user_get_or_create
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect 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): class GoogleOAuthRedirect(OAuthRedirect):
"""Google OAuth2 Redirect""" """Google OAuth2 Redirect"""
def get_additional_parameters(self, source): def get_additional_parameters(self, inlet):
return { return {
"scope": "email profile", "scope": "email profile",
} }
@MANAGER.source(kind=RequestKind.callback, name="Google") @MANAGER.inlet(kind=RequestKind.callback, name="Google")
class GoogleOAuth2Callback(OAuthCallback): class GoogleOAuth2Callback(OAuthCallback):
"""Google OAuth2 Callback""" """Google OAuth2 Callback"""
def get_or_create_user(self, source, access, info): def get_or_create_user(self, inlet, access, info):
user_data = { user_data = {
"username": info.get("email"), "username": info.get("email"),
"email": info.get("email", ""), "email": info.get("email", ""),

View File

@ -1,10 +1,10 @@
"""Source type manager""" """Inlet type manager"""
from enum import Enum from enum import Enum
from django.utils.text import slugify from django.utils.text import slugify
from structlog import get_logger 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() LOGGER = get_logger()
@ -16,21 +16,21 @@ class RequestKind(Enum):
redirect = "redirect" redirect = "redirect"
class SourceTypeManager: class InletTypeManager:
"""Manager to hold all Source types.""" """Manager to hold all Inlet types."""
__source_types = {} __inlet_types = {}
__names = [] __names = []
def source(self, kind, name): def inlet(self, kind, name):
"""Class decorator to register classes inline.""" """Class decorator to register classes inline."""
def inner_wrapper(cls): def inner_wrapper(cls):
if kind not in self.__source_types: if kind not in self.__inlet_types:
self.__source_types[kind] = {} self.__inlet_types[kind] = {}
self.__source_types[kind][name.lower()] = cls self.__inlet_types[kind][name.lower()] = cls
self.__names.append(name) 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 cls
return inner_wrapper return inner_wrapper
@ -39,11 +39,11 @@ class SourceTypeManager:
"""Get list of tuples of all registered names""" """Get list of tuples of all registered names"""
return [(slugify(x), x) for x in set(self.__names)] return [(slugify(x), x) for x in set(self.__names)]
def find(self, source, kind): def find(self, inlet, kind):
"""Find fitting Source Type""" """Find fitting Inlet Type"""
if kind in self.__source_types: if kind in self.__inlet_types:
if source.provider_type in self.__source_types[kind]: if inlet.provider_type in self.__inlet_types[kind]:
return self.__source_types[kind][source.provider_type] return self.__inlet_types[kind][inlet.provider_type]
# Return defaults # Return defaults
if kind == RequestKind.callback: if kind == RequestKind.callback:
return OAuthCallback return OAuthCallback
@ -52,4 +52,4 @@ class SourceTypeManager:
raise KeyError raise KeyError
MANAGER = SourceTypeManager() MANAGER = InletTypeManager()

View File

@ -1,17 +1,17 @@
"""Reddit OAuth Views""" """Reddit OAuth Views"""
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from passbook.sources.oauth.clients import OAuth2Client from passbook.channels.in_oauth.clients import OAuth2Client
from passbook.sources.oauth.types.manager import MANAGER, RequestKind from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
from passbook.sources.oauth.utils import user_get_or_create from passbook.channels.in_oauth.utils import user_get_or_create
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect 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): class RedditOAuthRedirect(OAuthRedirect):
"""Reddit OAuth2 Redirect""" """Reddit OAuth2 Redirect"""
def get_additional_parameters(self, source): def get_additional_parameters(self, inlet):
return { return {
"scope": "identity", "scope": "identity",
"duration": "permanent", "duration": "permanent",
@ -23,19 +23,19 @@ class RedditOAuth2Client(OAuth2Client):
def get_access_token(self, request, callback=None, **request_kwargs): def get_access_token(self, request, callback=None, **request_kwargs):
"Fetch access token from callback request." "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( return super(RedditOAuth2Client, self).get_access_token(
request, callback, auth=auth request, callback, auth=auth
) )
@MANAGER.source(kind=RequestKind.callback, name="reddit") @MANAGER.inlet(kind=RequestKind.callback, name="reddit")
class RedditOAuth2Callback(OAuthCallback): class RedditOAuth2Callback(OAuthCallback):
"""Reddit OAuth2 Callback""" """Reddit OAuth2 Callback"""
client_class = RedditOAuth2Client client_class = RedditOAuth2Client
def get_or_create_user(self, source, access, info): def get_or_create_user(self, inlet, access, info):
user_data = { user_data = {
"username": info.get("name"), "username": info.get("name"),
"email": None, "email": None,

View File

@ -1,14 +1,14 @@
"""Twitter OAuth Views""" """Twitter OAuth Views"""
from passbook.sources.oauth.types.manager import MANAGER, RequestKind from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
from passbook.sources.oauth.utils import user_get_or_create from passbook.channels.in_oauth.utils import user_get_or_create
from passbook.sources.oauth.views.core import OAuthCallback 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): class TwitterOAuthCallback(OAuthCallback):
"""Twitter OAuth2 Callback""" """Twitter OAuth2 Callback"""
def get_or_create_user(self, source, access, info): def get_or_create_user(self, inlet, access, info):
user_data = { user_data = {
"username": info.get("screen_name"), "username": info.get("screen_name"),
"email": info.get("email", ""), "email": info.get("email", ""),

View File

@ -2,27 +2,27 @@
from django.urls import path from django.urls import path
from passbook.sources.oauth.types.manager import RequestKind from passbook.channels.in_oauth.types.manager import RequestKind
from passbook.sources.oauth.views import core, dispatcher, user from passbook.channels.in_oauth.views import core, dispatcher, user
urlpatterns = [ urlpatterns = [
path( path(
"login/<slug:source_slug>/", "login/<slug:inlet_slug>/",
dispatcher.DispatcherView.as_view(kind=RequestKind.redirect), dispatcher.DispatcherView.as_view(kind=RequestKind.redirect),
name="oauth-client-login", name="oauth-client-login",
), ),
path( path(
"callback/<slug:source_slug>/", "callback/<slug:inlet_slug>/",
dispatcher.DispatcherView.as_view(kind=RequestKind.callback), dispatcher.DispatcherView.as_view(kind=RequestKind.callback),
name="oauth-client-callback", name="oauth-client-callback",
), ),
path( path(
"disconnect/<slug:source_slug>/", "disconnect/<slug:inlet_slug>/",
core.DisconnectView.as_view(), core.DisconnectView.as_view(),
name="oauth-client-disconnect", name="oauth-client-disconnect",
), ),
path( path(
"user/<slug:source_slug>/", "user/<slug:inlet_slug>/",
user.UserSettingsView.as_view(), user.UserSettingsView.as_view(),
name="oauth-client-user", name="oauth-client-user",
), ),

View File

@ -13,6 +13,8 @@ from django.views.generic import RedirectView, View
from structlog import get_logger from structlog import get_logger
from passbook.audit.models import Event, EventAction 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.models import Flow, FlowDesignation
from passbook.flows.planner import ( from passbook.flows.planner import (
PLAN_CONTEXT_PENDING_USER, PLAN_CONTEXT_PENDING_USER,
@ -21,8 +23,6 @@ from passbook.flows.planner import (
) )
from passbook.flows.views import SESSION_KEY_PLAN from passbook.flows.views import SESSION_KEY_PLAN
from passbook.lib.utils.urls import redirect_with_qs 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 from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
LOGGER = get_logger() LOGGER = get_logger()
@ -30,49 +30,49 @@ LOGGER = get_logger()
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class OAuthClientMixin: class OAuthClientMixin:
"Mixin for getting OAuth client for a source." "Mixin for getting OAuth client for a inlet."
client_class: Optional[Callable] = None client_class: Optional[Callable] = None
def get_client(self, source): def get_client(self, inlet):
"Get instance of the OAuth client for this source." "Get instance of the OAuth client for this inlet."
if self.client_class is not None: if self.client_class is not None:
# pylint: disable=not-callable # pylint: disable=not-callable
return self.client_class(source) return self.client_class(inlet)
return get_client(source) return get_client(inlet)
class OAuthRedirect(OAuthClientMixin, RedirectView): class OAuthRedirect(OAuthClientMixin, RedirectView):
"Redirect user to OAuth source to enable access." "Redirect user to OAuth inlet to enable access."
permanent = False permanent = False
params = None params = None
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_additional_parameters(self, source): def get_additional_parameters(self, inlet):
"Return additional redirect parameters for this source." "Return additional redirect parameters for this inlet."
return self.params or {} return self.params or {}
def get_callback_url(self, source): def get_callback_url(self, inlet):
"Return the callback url for this source." "Return the callback url for this inlet."
return reverse( return reverse(
"passbook_sources_oauth:oauth-client-callback", "passbook_channels_in_oauth:oauth-client-callback",
kwargs={"source_slug": source.slug}, kwargs={"inlet_slug": inlet.slug},
) )
def get_redirect_url(self, **kwargs): def get_redirect_url(self, **kwargs):
"Build redirect url for a given source." "Build redirect url for a given inlet."
slug = kwargs.get("source_slug", "") slug = kwargs.get("inlet_slug", "")
try: try:
source = OAuthSource.objects.get(slug=slug) inlet = OAuthInlet.objects.get(slug=slug)
except OAuthSource.DoesNotExist: except OAuthInlet.DoesNotExist:
raise Http404("Unknown OAuth source '%s'." % slug) raise Http404("Unknown OAuth inlet '%s'." % slug)
else: else:
if not source.enabled: if not inlet.enabled:
raise Http404("source %s is not enabled." % slug) raise Http404("inlet %s is not enabled." % slug)
client = self.get_client(source) client = self.get_client(inlet)
callback = self.get_callback_url(source) callback = self.get_callback_url(inlet)
params = self.get_additional_parameters(source) params = self.get_additional_parameters(inlet)
return client.get_redirect_url( return client.get_redirect_url(
self.request, callback=callback, parameters=params self.request, callback=callback, parameters=params
) )
@ -81,85 +81,85 @@ class OAuthRedirect(OAuthClientMixin, RedirectView):
class OAuthCallback(OAuthClientMixin, View): class OAuthCallback(OAuthClientMixin, View):
"Base OAuth callback view." "Base OAuth callback view."
source_id = None inlet_id = None
source = None inlet = None
def get(self, request, *_, **kwargs): def get(self, request, *_, **kwargs):
"""View Get handler""" """View Get handler"""
slug = kwargs.get("source_slug", "") slug = kwargs.get("inlet_slug", "")
try: try:
self.source = OAuthSource.objects.get(slug=slug) self.inlet = OAuthInlet.objects.get(slug=slug)
except OAuthSource.DoesNotExist: except OAuthInlet.DoesNotExist:
raise Http404("Unknown OAuth source '%s'." % slug) raise Http404("Unknown OAuth inlet '%s'." % slug)
else: else:
if not self.source.enabled: if not self.inlet.enabled:
raise Http404("source %s is not enabled." % slug) raise Http404("inlet %s is not enabled." % slug)
client = self.get_client(self.source) client = self.get_client(self.inlet)
callback = self.get_callback_url(self.source) callback = self.get_callback_url(self.inlet)
# Fetch access token # Fetch access token
token = client.get_access_token(self.request, callback=callback) token = client.get_access_token(self.request, callback=callback)
if token is None: if token is None:
return self.handle_login_failure( return self.handle_login_failure(
self.source, "Could not retrieve token." self.inlet, "Could not retrieve token."
) )
if "error" in 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 # Fetch profile info
info = client.get_profile_info(token) info = client.get_profile_info(token)
if info is None: if info is None:
return self.handle_login_failure( 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: 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 # Get or create access record
defaults = { defaults = {
"access_token": token.get("access_token"), "access_token": token.get("access_token"),
} }
existing = UserOAuthSourceConnection.objects.filter( existing = UserOAuthInletConnection.objects.filter(
source=self.source, identifier=identifier inlet=self.inlet, identifier=identifier
) )
if existing.exists(): if existing.exists():
connection = existing.first() connection = existing.first()
connection.access_token = token.get("access_token") connection.access_token = token.get("access_token")
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update( UserOAuthInletConnection.objects.filter(pk=connection.pk).update(
**defaults **defaults
) )
else: else:
connection = UserOAuthSourceConnection( connection = UserOAuthInletConnection(
source=self.source, inlet=self.inlet,
identifier=identifier, identifier=identifier,
access_token=token.get("access_token"), access_token=token.get("access_token"),
) )
user = authenticate( user = authenticate(
source=self.source, identifier=identifier, request=request inlet=self.inlet, identifier=identifier, request=request
) )
if user is None: if user is None:
LOGGER.debug("Handling new user", source=self.source) LOGGER.debug("Handling new user", inlet=self.inlet)
return self.handle_new_user(self.source, connection, info) return self.handle_new_user(self.inlet, connection, info)
LOGGER.debug("Handling existing user", source=self.source) LOGGER.debug("Handling existing user", inlet=self.inlet)
return self.handle_existing_user(self.source, user, connection, info) return self.handle_existing_user(self.inlet, user, connection, info)
# pylint: disable=unused-argument # 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 callback url if different than the current url."
return False return False
# pylint: disable=unused-argument # 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 url to redirect on login failure."
return settings.LOGIN_URL 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." "Create a shell auth.User."
raise NotImplementedError() raise NotImplementedError()
# pylint: disable=unused-argument # 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." "Return unique identifier from the profile info."
id_key = self.source_id or "id" id_key = self.inlet_id or "id"
result = info result = info
try: try:
for key in id_key.split("."): for key in id_key.split("."):
@ -168,10 +168,10 @@ class OAuthCallback(OAuthClientMixin, View):
except KeyError: except KeyError:
return None return None
def handle_login(self, user, source, access): def handle_login(self, user, inlet, access):
"""Prepare Authentication Plan, redirect user FlowExecutor""" """Prepare Authentication Plan, redirect user FlowExecutor"""
user = authenticate( 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 # 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) flow = get_object_or_404(Flow, designation=FlowDesignation.AUTHENTICATION)
@ -186,24 +186,24 @@ class OAuthCallback(OAuthClientMixin, View):
) )
# pylint: disable=unused-argument # 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." "Login user and redirect."
messages.success( messages.success(
self.request, self.request,
_( _(
"Successfully authenticated with %(source)s!" "Successfully authenticated with %(inlet)s!"
% {"source": self.source.name} % {"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." "Message user and redirect on error."
LOGGER.warning("Authentication Failure", reason=reason) LOGGER.warning("Authentication Failure", reason=reason)
messages.error(self.request, _("Authentication Failed.")) 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." "Create a shell auth.User and redirect."
was_authenticated = False was_authenticated = False
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
@ -211,52 +211,52 @@ class OAuthCallback(OAuthClientMixin, View):
user = self.request.user user = self.request.user
was_authenticated = True was_authenticated = True
else: else:
user = self.get_or_create_user(source, access, info) user = self.get_or_create_user(inlet, access, info)
access.user = user access.user = user
access.save() access.save()
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user) UserOAuthInletConnection.objects.filter(pk=access.pk).update(user=user)
Event.new( Event.new(
EventAction.CUSTOM, message="Linked OAuth Source", source=source EventAction.CUSTOM, message="Linked OAuth Inlet", inlet=inlet
).from_http(self.request) ).from_http(self.request)
if was_authenticated: if was_authenticated:
messages.success( messages.success(
self.request, self.request,
_("Successfully linked %(source)s!" % {"source": self.source.name}), _("Successfully linked %(inlet)s!" % {"inlet": self.inlet.name}),
) )
return redirect( return redirect(
reverse( reverse(
"passbook_sources_oauth:oauth-client-user", "passbook_channels_in_oauth:oauth-client-user",
kwargs={"source_slug": self.source.slug}, kwargs={"inlet_slug": self.inlet.slug},
) )
) )
# User was not authenticated, new user has been created # User was not authenticated, new user has been created
user = authenticate( user = authenticate(
source=access.source, identifier=access.identifier, request=self.request inlet=access.inlet, identifier=access.identifier, request=self.request
) )
messages.success( messages.success(
self.request, self.request,
_( _(
"Successfully authenticated with %(source)s!" "Successfully authenticated with %(inlet)s!"
% {"source": self.source.name} % {"inlet": self.inlet.name}
), ),
) )
return self.handle_login(user, source, access) return self.handle_login(user, inlet, access)
class DisconnectView(LoginRequiredMixin, View): class DisconnectView(LoginRequiredMixin, View):
"""Delete connection with source""" """Delete connection with inlet"""
source = None inlet = None
aas = None aas = None
def dispatch(self, request, source_slug): def dispatch(self, request, inlet_slug):
self.source = get_object_or_404(OAuthSource, slug=source_slug) self.inlet = get_object_or_404(OAuthInlet, slug=inlet_slug)
self.aas = get_object_or_404( 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""" """Delete connection object"""
if "confirmdelete" in request.POST: if "confirmdelete" in request.POST:
# User confirmed deletion # User confirmed deletion
@ -264,23 +264,23 @@ class DisconnectView(LoginRequiredMixin, View):
messages.success(request, _("Connection successfully deleted")) messages.success(request, _("Connection successfully deleted"))
return redirect( return redirect(
reverse( reverse(
"passbook_sources_oauth:oauth-client-user", "passbook_channels_in_oauth:oauth-client-user",
kwargs={"source_slug": self.source.slug}, kwargs={"inlet_slug": self.inlet.slug},
) )
) )
return self.get(request, source_slug) return self.get(request, inlet_slug)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get(self, request, source_slug): def get(self, request, inlet_slug):
"""Show delete form""" """Show delete form"""
return render( return render(
request, request,
"generic/delete.html", "generic/delete.html",
{ {
"object": self.source, "object": self.inlet,
"delete_url": reverse( "delete_url": reverse(
"passbook_sources_oauth:oauth-client-disconnect", "passbook_channels_in_oauth:oauth-client-disconnect",
kwargs={"source_slug": self.source.slug,}, kwargs={"inlet_slug": self.inlet.slug,},
), ),
}, },
) )

View File

@ -3,8 +3,8 @@ from django.http import Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.views import View from django.views import View
from passbook.sources.oauth.models import OAuthSource from passbook.channels.in_oauth.models import OAuthInlet
from passbook.sources.oauth.types.manager import MANAGER, RequestKind from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
class DispatcherView(View): class DispatcherView(View):
@ -13,10 +13,10 @@ class DispatcherView(View):
kind = "" kind = ""
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
"""Find Source by slug and forward request""" """Find Inlet by slug and forward request"""
slug = kwargs.get("source_slug", None) slug = kwargs.get("inlet_slug", None)
if not slug: if not slug:
raise Http404 raise Http404
source = get_object_or_404(OAuthSource, slug=slug) inlet = get_object_or_404(OAuthInlet, slug=slug)
view = MANAGER.find(source, kind=RequestKind(self.kind)) view = MANAGER.find(inlet, kind=RequestKind(self.kind))
return view.as_view()(*args, **kwargs) return view.as_view()(*args, **kwargs)

View File

@ -3,7 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.views.generic import TemplateView 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): class UserSettingsView(LoginRequiredMixin, TemplateView):
@ -12,10 +12,10 @@ class UserSettingsView(LoginRequiredMixin, TemplateView):
template_name = "oauth_client/user.html" template_name = "oauth_client/user.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
source = get_object_or_404(OAuthSource, slug=self.kwargs.get("source_slug")) inlet = get_object_or_404(OAuthInlet, slug=self.kwargs.get("inlet_slug"))
connections = UserOAuthSourceConnection.objects.filter( connections = UserOAuthInletConnection.objects.filter(
user=self.request.user, source=source user=self.request.user, inlet=inlet
) )
kwargs["source"] = source kwargs["inlet"] = inlet
kwargs["connections"] = connections kwargs["connections"] = connections
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View 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

View 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/"

View File

@ -4,17 +4,17 @@ from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.admin.forms.source import SOURCE_FORM_FIELDS from passbook.admin.forms.inlet import INLET_FORM_FIELDS
from passbook.sources.saml.models import SAMLSource from passbook.channels.in_saml.models import SAMLInlet
class SAMLSourceForm(forms.ModelForm): class SAMLInletForm(forms.ModelForm):
"""SAML Provider form""" """SAML Inlet form"""
class Meta: class Meta:
model = SAMLSource model = SAMLInlet
fields = SOURCE_FORM_FIELDS + [ fields = INLET_FORM_FIELDS + [
"issuer", "issuer",
"idp_url", "idp_url",
"idp_logout_url", "idp_logout_url",

View 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",),
),
]

View File

@ -3,13 +3,13 @@ from django.db import models
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ 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.core.types import UILoginButton
from passbook.crypto.models import CertificateKeyPair from passbook.crypto.models import CertificateKeyPair
class SAMLSource(Source): class SAMLInlet(Inlet):
"""SAML Source""" """SAML Inlet"""
issuer = models.TextField( issuer = models.TextField(
blank=True, blank=True,
@ -34,14 +34,14 @@ class SAMLSource(Source):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
form = "passbook.sources.saml.forms.SAMLSourceForm" form = "passbook.channels.in_saml.forms.SAMLInletForm"
@property @property
def ui_login_button(self) -> UILoginButton: def ui_login_button(self) -> UILoginButton:
return UILoginButton( return UILoginButton(
name=self.name, name=self.name,
url=reverse_lazy( url=reverse_lazy(
"passbook_sources_saml:login", kwargs={"source_slug": self.slug} "passbook_channels_in_saml:login", kwargs={"inlet_slug": self.slug}
), ),
icon_path="", icon_path="",
) )
@ -49,14 +49,14 @@ class SAMLSource(Source):
@property @property
def ui_additional_info(self) -> str: def ui_additional_info(self) -> str:
metadata_url = reverse_lazy( 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>' return f'<a href="{metadata_url}" class="btn btn-default btn-sm">Metadata Download</a>'
def __str__(self): def __str__(self):
return f"SAML Source {self.name}" return f"SAML Inlet {self.name}"
class Meta: class Meta:
verbose_name = _("SAML Source") verbose_name = _("SAML Inlet")
verbose_name_plural = _("SAML Sources") verbose_name_plural = _("SAML Inlets")

View File

@ -1,4 +1,4 @@
"""passbook saml source processor""" """passbook saml inlet processor"""
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from defusedxml import ElementTree from defusedxml import ElementTree
@ -6,13 +6,13 @@ from django.http import HttpRequest
from signxml import XMLVerifier from signxml import XMLVerifier
from structlog import get_logger from structlog import get_logger
from passbook.core.models import User from passbook.channels.in_saml.exceptions import (
from passbook.providers.saml.utils.encoding import decode_base64_and_inflate
from passbook.sources.saml.exceptions import (
MissingSAMLResponse, MissingSAMLResponse,
UnsupportedNameIDFormat, 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() LOGGER = get_logger()
if TYPE_CHECKING: if TYPE_CHECKING:
@ -22,13 +22,13 @@ if TYPE_CHECKING:
class Processor: class Processor:
"""SAML Response Processor""" """SAML Response Processor"""
_source: SAMLSource _inlet: SAMLInlet
_root: "Element" _root: "Element"
_root_xml: str _root_xml: str
def __init__(self, source: SAMLSource): def __init__(self, inlet: SAMLInlet):
self._source = source self._inlet = inlet
def parse(self, request: HttpRequest): def parse(self, request: HttpRequest):
"""Check if `request` contains SAML Response data, parse and validate it.""" """Check if `request` contains SAML Response data, parse and validate it."""
@ -46,7 +46,7 @@ class Processor:
def _verify_signed(self): def _verify_signed(self):
"""Verify SAML Response's Signature""" """Verify SAML Response's Signature"""
verifier = XMLVerifier() 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]: def _get_email(self) -> Optional[str]:
""" """

View File

@ -1,7 +1,7 @@
"""saml sp urls""" """saml sp urls"""
from django.urls import path 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 = [ urlpatterns = [
path("<slug:source_slug>/", InitiateView.as_view(), name="login"), path("<slug:source_slug>/", InitiateView.as_view(), name="login"),

View 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})
)

View File

@ -7,36 +7,36 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from signxml.util import strip_pem_header from signxml.util import strip_pem_header
from passbook.lib.views import bad_request_message from passbook.channels.in_saml.exceptions import (
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 (
MissingSAMLResponse, MissingSAMLResponse,
UnsupportedNameIDFormat, UnsupportedNameIDFormat,
) )
from passbook.sources.saml.models import SAMLSource from passbook.channels.in_saml.models import SAMLInlet
from passbook.sources.saml.processors.base import Processor from passbook.channels.in_saml.processors.base import Processor
from passbook.sources.saml.utils import build_full_url, get_issuer from passbook.channels.in_saml.utils import build_full_url, get_issuer
from passbook.sources.saml.xml_render import get_authnrequest_xml 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): class InitiateView(View):
"""Get the Form with SAML Request, which sends us to the IDP""" """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.""" """Replies with an XHTML SSO Request."""
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug)
if not source.enabled: if not inlet.enabled:
raise Http404 raise Http404
sso_destination = request.GET.get("next", None) sso_destination = request.GET.get("next", None)
request.session["sso_destination"] = sso_destination request.session["sso_destination"] = sso_destination
parameters = { parameters = {
"ACS_URL": build_full_url("acs", request, source), "ACS_URL": build_full_url("acs", request, inlet),
"DESTINATION": source.idp_url, "DESTINATION": inlet.idp_url,
"AUTHN_REQUEST_ID": get_random_id(), "AUTHN_REQUEST_ID": get_random_id(),
"ISSUE_INSTANT": get_time_string(), "ISSUE_INSTANT": get_time_string(),
"ISSUER": get_issuer(request, source), "ISSUER": get_issuer(request, inlet),
} }
authn_req = get_authnrequest_xml(parameters, signed=False) authn_req = get_authnrequest_xml(parameters, signed=False)
_request = nice64(str.encode(authn_req)) _request = nice64(str.encode(authn_req))
@ -44,10 +44,10 @@ class InitiateView(View):
request, request,
"saml/sp/login.html", "saml/sp/login.html",
{ {
"request_url": source.idp_url, "request_url": inlet.idp_url,
"request": _request, "request": _request,
"token": sso_destination, "token": sso_destination,
"source": source, "inlet": inlet,
}, },
) )
@ -56,12 +56,12 @@ class InitiateView(View):
class ACSView(View): class ACSView(View):
"""AssertionConsumerService, consume assertion and log user in""" """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.""" """Handles a POSTed SSO Assertion and logs the user in."""
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug)
if not source.enabled: if not inlet.enabled:
raise Http404 raise Http404
processor = Processor(source) processor = Processor(inlet)
try: try:
processor.parse(request) processor.parse(request)
except MissingSAMLResponse as exc: except MissingSAMLResponse as exc:
@ -78,37 +78,34 @@ class ACSView(View):
class SLOView(View): class SLOView(View):
"""Single-Logout-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.""" """Replies with an XHTML SSO Request."""
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug)
if not source.enabled: if not inlet.enabled:
raise Http404 raise Http404
logout(request) logout(request)
return render( return render(
request, request,
"saml/sp/sso_single_logout.html", "saml/sp/sso_single_logout.html",
{ {"idp_logout_url": inlet.idp_logout_url, "autosubmit": inlet.auto_logout,},
"idp_logout_url": source.idp_logout_url,
"autosubmit": source.auto_logout,
},
) )
class MetadataView(View): class MetadataView(View):
"""Return XML Metadata for IDP""" """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.""" """Replies with the XML Metadata SPSSODescriptor."""
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug) inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug)
issuer = get_issuer(request, source) issuer = get_issuer(request, inlet)
cert_stripped = strip_pem_header( cert_stripped = strip_pem_header(
source.signing_kp.certificate_data.replace("\r", "") inlet.signing_kp.certificate_data.replace("\r", "")
).replace("\n", "") ).replace("\n", "")
return render_xml( return render_xml(
request, request,
"saml/sp/xml/sp_sso_descriptor.xml", "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, "issuer": issuer,
"cert_public_key": cert_stripped, "cert_public_key": cert_stripped,
}, },

View File

@ -1,8 +1,8 @@
"""Functions for creating XML output.""" """Functions for creating XML output."""
from structlog import get_logger 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.lib.utils.template import render_to_string
from passbook.providers.saml.utils.xml_signing import get_signature_xml
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -1,17 +1,17 @@
"""ApplicationGatewayProvider API Views""" """ApplicationGatewayOutlet API Views"""
from oauth2_provider.generators import generate_client_id, generate_client_secret from oauth2_provider.generators import generate_client_id, generate_client_secret
from oidc_provider.models import Client from oidc_provider.models import Client
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from passbook.providers.app_gw.models import ApplicationGatewayProvider from passbook.channels.out_app_gw.models import ApplicationGatewayOutlet
from passbook.providers.oidc.api import OpenIDProviderSerializer from passbook.channels.out_oidc.api import OpenIDOutletSerializer
class ApplicationGatewayProviderSerializer(ModelSerializer): class ApplicationGatewayOutletSerializer(ModelSerializer):
"""ApplicationGatewayProvider Serializer""" """ApplicationGatewayOutlet Serializer"""
client = OpenIDProviderSerializer() client = OpenIDOutletSerializer()
def create(self, validated_data): def create(self, validated_data):
instance = super().create(validated_data) instance = super().create(validated_data)
@ -33,13 +33,13 @@ class ApplicationGatewayProviderSerializer(ModelSerializer):
class Meta: class Meta:
model = ApplicationGatewayProvider model = ApplicationGatewayOutlet
fields = ["pk", "name", "internal_host", "external_host", "client"] fields = ["pk", "name", "internal_host", "external_host", "client"]
read_only_fields = ["client"] read_only_fields = ["client"]
class ApplicationGatewayProviderViewSet(ModelViewSet): class ApplicationGatewayOutletViewSet(ModelViewSet):
"""ApplicationGatewayProvider Viewset""" """ApplicationGatewayOutlet Viewset"""
queryset = ApplicationGatewayProvider.objects.all() queryset = ApplicationGatewayOutlet.objects.all()
serializer_class = ApplicationGatewayProviderSerializer serializer_class = ApplicationGatewayOutletSerializer

View File

@ -5,7 +5,7 @@ from django.apps import AppConfig
class PassbookApplicationApplicationGatewayConfig(AppConfig): class PassbookApplicationApplicationGatewayConfig(AppConfig):
"""passbook app_gw app""" """passbook app_gw app"""
name = "passbook.providers.app_gw" name = "passbook.channels.out_app_gw"
label = "passbook_providers_app_gw" label = "passbook_channels_out_app_gw"
verbose_name = "passbook Providers.Application Security Gateway" verbose_name = "passbook Outlets.Application Security Gateway"
mountpoint = "application/gateway/" mountpoint = "application/gateway/"

View File

@ -3,11 +3,11 @@ from django import forms
from oauth2_provider.generators import generate_client_id, generate_client_secret from oauth2_provider.generators import generate_client_id, generate_client_secret
from oidc_provider.models import Client, ResponseType 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): class ApplicationGatewayOutletForm(forms.ModelForm):
"""Security Gateway Provider form""" """Security Gateway Outlet form"""
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.instance.pk: if not self.instance.pk:
@ -31,7 +31,7 @@ class ApplicationGatewayProviderForm(forms.ModelForm):
class Meta: class Meta:
model = ApplicationGatewayProvider model = ApplicationGatewayOutlet
fields = ["name", "internal_host", "external_host"] fields = ["name", "internal_host", "external_host"]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),

View File

@ -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 import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -9,28 +9,28 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("passbook_core", "0005_merge_20191025_2022"), ("passbook_core", "__first__"),
("oidc_provider", "0026_client_multiple_response_types"), ("oidc_provider", "0026_client_multiple_response_types"),
("passbook_providers_app_gw", "0002_auto_20191111_1703"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="ApplicationGatewayProvider", name="ApplicationGatewayOutlet",
fields=[ fields=[
( (
"provider_ptr", "outlet_ptr",
models.OneToOneField( models.OneToOneField(
auto_created=True, auto_created=True,
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
parent_link=True, parent_link=True,
primary_key=True, primary_key=True,
serialize=False, serialize=False,
to="passbook_core.Provider", to="passbook_core.Outlet",
), ),
), ),
("name", models.TextField()), ("name", models.TextField()),
("host", models.TextField()), ("internal_host", models.TextField()),
("external_host", models.TextField()),
( (
"client", "client",
models.ForeignKey( models.ForeignKey(
@ -40,9 +40,9 @@ class Migration(migrations.Migration):
), ),
], ],
options={ options={
"verbose_name": "Application Gateway Provider", "verbose_name": "Application Gateway Outlet",
"verbose_name_plural": "Application Gateway Providers", "verbose_name_plural": "Application Gateway Outlets",
}, },
bases=("passbook_core.provider",), bases=("passbook_core.outlet",),
), ),
] ]

View File

@ -9,12 +9,12 @@ from django.utils.translation import gettext as _
from oidc_provider.models import Client from oidc_provider.models import Client
from passbook import __version__ 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 from passbook.lib.utils.template import render_to_string
class ApplicationGatewayProvider(Provider): class ApplicationGatewayOutlet(Outlet):
"""This provider uses oauth2_proxy with the OIDC Provider.""" """This outlet uses oauth2_proxy with the OIDC Outlet."""
name = models.TextField() name = models.TextField()
internal_host = models.TextField() internal_host = models.TextField()
@ -22,7 +22,7 @@ class ApplicationGatewayProvider(Provider):
client = models.ForeignKey(Client, on_delete=models.CASCADE) 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]: def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
"""return template and context modal with URLs for authorize, token, openid-config, etc""" """return template and context modal with URLs for authorize, token, openid-config, etc"""
@ -32,7 +32,7 @@ class ApplicationGatewayProvider(Provider):
) )
return render_to_string( return render_to_string(
"app_gw/setup_modal.html", "app_gw/setup_modal.html",
{"provider": self, "cookie_secret": cookie_secret, "version": __version__}, {"outlet": self, "cookie_secret": cookie_secret, "version": __version__},
) )
def __str__(self): def __str__(self):
@ -40,5 +40,5 @@ class ApplicationGatewayProvider(Provider):
class Meta: class Meta:
verbose_name = _("Application Gateway Provider") verbose_name = _("Application Gateway Outlet")
verbose_name_plural = _("Application Gateway Providers") verbose_name_plural = _("Application Gateway Outlets")

View File

@ -42,7 +42,7 @@
<h1 class="pf-c-title pf-m-2xl">{% trans 'Setup with Kubernetes' %}</h1> <h1 class="pf-c-title pf-m-2xl">{% trans 'Setup with Kubernetes' %}</h1>
<div class="pf-c-modal-box__body"> <div class="pf-c-modal-box__body">
<p>{% trans 'Download the manifest to create the Gatekeeper deployment and service:' %}</p> <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> <p>{% trans 'Afterwards, add the following annotations to the Ingress you want to secure:' %}</p>
<textarea class="codemirror" readonly data-cm-mode="yaml"> <textarea class="codemirror" readonly data-cm-mode="yaml">
nginx.ingress.kubernetes.io/auth-url: "https://{{ provider.external_host }}/oauth2/auth" nginx.ingress.kubernetes.io/auth-url: "https://{{ provider.external_host }}/oauth2/auth"

View File

@ -1,7 +1,7 @@
"""passbook app_gw urls""" """passbook app_gw urls"""
from django.urls import path from django.urls import path
from passbook.providers.app_gw.views import K8sManifestView from passbook.channels.out_app_gw.views import K8sManifestView
urlpatterns = [ urlpatterns = [
path( path(

View File

@ -9,7 +9,7 @@ from django.views import View
from structlog import get_logger from structlog import get_logger
from passbook import __version__ 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" ORIGINAL_URL = "HTTP_X_ORIGINAL_URL"
LOGGER = get_logger() LOGGER = get_logger()
@ -25,14 +25,14 @@ def get_cookie_secret():
class K8sManifestView(LoginRequiredMixin, View): class K8sManifestView(LoginRequiredMixin, View):
"""Generate K8s Deployment and SVC for gatekeeper""" """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""" """Render deployment template"""
provider = get_object_or_404(ApplicationGatewayProvider, pk=provider) outlet = get_object_or_404(ApplicationGatewayOutlet, pk=outlet)
return render( return render(
request, request,
"app_gw/k8s-manifest.yaml", "app_gw/k8s-manifest.yaml",
{ {
"provider": provider, "outlet": outlet,
"cookie_secret": get_cookie_secret(), "cookie_secret": get_cookie_secret(),
"version": __version__, "version": __version__,
}, },

View 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

View 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 = ""

View File

@ -1,16 +1,16 @@
"""passbook OAuth2 Provider Forms""" """passbook OAuth2 Outlet Forms"""
from django import forms from django import forms
from passbook.providers.oauth.models import OAuth2Provider from passbook.channels.out_oauth.models import OAuth2Outlet
class OAuth2ProviderForm(forms.ModelForm): class OAuth2OutletForm(forms.ModelForm):
"""OAuth2 Provider form""" """OAuth2 Outlet form"""
class Meta: class Meta:
model = OAuth2Provider model = OAuth2Outlet
fields = [ fields = [
"name", "name",
"redirect_uris", "redirect_uris",

View File

@ -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 django.db.models.deletion
import oauth2_provider.generators import oauth2_provider.generators
@ -16,22 +16,22 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("passbook_core", "0001_initial"), ("passbook_core", "__first__"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="OAuth2Provider", name="OAuth2Outlet",
fields=[ fields=[
( (
"provider_ptr", "outlet_ptr",
models.OneToOneField( models.OneToOneField(
auto_created=True, auto_created=True,
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
parent_link=True, parent_link=True,
primary_key=True, primary_key=True,
serialize=False, serialize=False,
to="passbook_core.Provider", to="passbook_core.Outlet",
), ),
), ),
( (
@ -90,15 +90,15 @@ class Migration(migrations.Migration):
blank=True, blank=True,
null=True, null=True,
on_delete=django.db.models.deletion.CASCADE, 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, to=settings.AUTH_USER_MODEL,
), ),
), ),
], ],
options={ options={
"verbose_name": "OAuth2 Provider", "verbose_name": "OAuth2 Outlet",
"verbose_name_plural": "OAuth2 Providers", "verbose_name_plural": "OAuth2 Outlets",
}, },
bases=("passbook_core.provider", models.Model), bases=("passbook_core.outlet", models.Model),
), ),
] ]

View File

@ -7,17 +7,17 @@ from django.shortcuts import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from oauth2_provider.models import AbstractApplication 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 from passbook.lib.utils.template import render_to_string
class OAuth2Provider(Provider, AbstractApplication): class OAuth2Outlet(Outlet, AbstractApplication):
"""Associate an OAuth2 Application with a Product""" """Associate an OAuth2 Application with a Product"""
form = "passbook.providers.oauth.forms.OAuth2ProviderForm" form = "passbook.channels.out_oauth.forms.OAuth2OutletForm"
def __str__(self): def __str__(self):
return f"OAuth2 Provider {self.name}" return f"OAuth2 Outlet {self.name}"
def html_setup_urls(self, request: HttpRequest) -> Optional[str]: def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
"""return template and context modal with URLs for authorize, token, openid-config, etc""" """return template and context modal with URLs for authorize, token, openid-config, etc"""
@ -26,10 +26,10 @@ class OAuth2Provider(Provider, AbstractApplication):
{ {
"provider": self, "provider": self,
"authorize_url": request.build_absolute_uri( "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( "token_url": request.build_absolute_uri(
reverse("passbook_providers_oauth:token") reverse("passbook_channels_out_oauth:token")
), ),
"userinfo_url": request.build_absolute_uri( "userinfo_url": request.build_absolute_uri(
reverse("passbook_api:openid") reverse("passbook_api:openid")
@ -39,5 +39,5 @@ class OAuth2Provider(Provider, AbstractApplication):
class Meta: class Meta:
verbose_name = _("OAuth2 Provider") verbose_name = _("OAuth2 Outlet")
verbose_name_plural = _("OAuth2 Providers") verbose_name_plural = _("OAuth2 Outlets")

View File

@ -1,4 +1,4 @@
"""passbook OAuth_Provider""" """passbook OAuth_Outlet"""
from django.conf import settings from django.conf import settings
CORS_ORIGIN_ALLOW_ALL = settings.DEBUG CORS_ORIGIN_ALLOW_ALL = settings.DEBUG
@ -17,7 +17,7 @@ AUTHENTICATION_BACKENDS = [
"oauth2_provider.backends.OAuth2Backend", "oauth2_provider.backends.OAuth2Backend",
] ]
OAUTH2_PROVIDER_APPLICATION_MODEL = "passbook_providers_oauth.OAuth2Provider" OAUTH2_PROVIDER_APPLICATION_MODEL = "passbook_channels_out_oauth.OAuth2Outlet"
OAUTH2_PROVIDER = { OAUTH2_PROVIDER = {
# this is the list of available scopes # this is the list of available scopes

Some files were not shown because too many files have changed in this diff Show More