*: providers and sources -> channels, PolicyModel to PolicyBindingModel that uses custom M2M through
This commit is contained in:
parent
615cd7870d
commit
7ed3ceb960
|
@ -0,0 +1,4 @@
|
||||||
|
"""passbook core inlet form fields"""
|
||||||
|
|
||||||
|
INLET_FORM_FIELDS = ["name", "slug", "enabled"]
|
||||||
|
INLET_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"]
|
|
@ -1,4 +0,0 @@
|
||||||
"""passbook core source form fields"""
|
|
||||||
|
|
||||||
SOURCE_FORM_FIELDS = ["name", "slug", "enabled"]
|
|
||||||
SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled"]
|
|
|
@ -8,12 +8,12 @@ from passbook.admin.views import (
|
||||||
debug,
|
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"),
|
||||||
|
|
|
@ -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):
|
|
@ -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):
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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):
|
|
@ -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})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Generated by Django 2.2.6 on 2019-10-28 08:29
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
("passbook_audit", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameModel(old_name="AuditEntry", new_name="Event",),
|
|
||||||
]
|
|
|
@ -1,40 +0,0 @@
|
||||||
# Generated by Django 2.2.8 on 2019-12-05 14:07
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import passbook.audit.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("passbook_audit", "0002_auto_20191028_0829"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="event",
|
|
||||||
options={
|
|
||||||
"verbose_name": "Audit Event",
|
|
||||||
"verbose_name_plural": "Audit Events",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="event",
|
|
||||||
name="action",
|
|
||||||
field=models.TextField(
|
|
||||||
choices=[
|
|
||||||
("LOGIN", "login"),
|
|
||||||
("LOGIN_FAILED", "login_failed"),
|
|
||||||
("LOGOUT", "logout"),
|
|
||||||
("AUTHORIZE_APPLICATION", "authorize_application"),
|
|
||||||
("SUSPICIOUS_REQUEST", "suspicious_request"),
|
|
||||||
("SIGN_UP", "sign_up"),
|
|
||||||
("PASSWORD_RESET", "password_reset"),
|
|
||||||
("INVITE_CREATED", "invitation_created"),
|
|
||||||
("INVITE_USED", "invitation_used"),
|
|
||||||
("CUSTOM", "custom"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Generated by Django 2.2.8 on 2019-12-05 15:02
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("passbook_audit", "0003_auto_20191205_1407"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(model_name="event", name="request_ip",),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="event",
|
|
||||||
name="client_ip",
|
|
||||||
field=models.GenericIPAddressField(null=True),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -5,7 +5,7 @@ from django.test import TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from 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):
|
||||||
|
|
|
@ -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):
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""Passbook ldap app config"""
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PassbookInletLDAPConfig(AppConfig):
|
||||||
|
"""Passbook ldap app config"""
|
||||||
|
|
||||||
|
name = "passbook.channels.in_ldap"
|
||||||
|
label = "passbook_channels_in_ldap"
|
||||||
|
verbose_name = "passbook Inlets.LDAP"
|
|
@ -3,8 +3,8 @@ from django.contrib.auth.backends import ModelBackend
|
||||||
from django.http import HttpRequest
|
from 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
|
|
@ -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"
|
|
@ -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",
|
|
@ -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",),
|
||||||
),
|
),
|
||||||
]
|
]
|
|
@ -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}"
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""OAuth Inlet Serializer"""
|
||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from passbook.admin.forms.inlet import INLET_SERIALIZER_FIELDS
|
||||||
|
from passbook.channels.in_oauth.models import OAuthInlet
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthInletSerializer(ModelSerializer):
|
||||||
|
"""OAuth Inlet Serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OAuthInlet
|
||||||
|
fields = INLET_SERIALIZER_FIELDS + [
|
||||||
|
"inlet_type",
|
||||||
|
"request_token_url",
|
||||||
|
"authorization_url",
|
||||||
|
"access_token_url",
|
||||||
|
"profile_url",
|
||||||
|
"consumer_key",
|
||||||
|
"consumer_secret",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthInletViewSet(ModelViewSet):
|
||||||
|
"""Inlet Viewset"""
|
||||||
|
|
||||||
|
queryset = OAuthInlet.objects.all()
|
||||||
|
serializer_class = OAuthInletSerializer
|
|
@ -8,12 +8,12 @@ from structlog import get_logger
|
||||||
LOGGER = get_logger()
|
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):
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""passbook oauth_client Authorization backend"""
|
||||||
|
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from passbook.channels.in_oauth.models import OAuthInlet, UserOAuthInletConnection
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizedServiceBackend(ModelBackend):
|
||||||
|
"Authentication backend for users registered with remote OAuth provider."
|
||||||
|
|
||||||
|
def authenticate(self, request, inlet=None, identifier=None):
|
||||||
|
"Fetch user for a given inlet by id."
|
||||||
|
inlet_q = Q(inlet__name=inlet)
|
||||||
|
if isinstance(inlet, OAuthInlet):
|
||||||
|
inlet_q = Q(inlet=inlet)
|
||||||
|
try:
|
||||||
|
access = UserOAuthInletConnection.objects.filter(
|
||||||
|
inlet_q, identifier=identifier
|
||||||
|
).select_related("user")[0]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return access.user
|
|
@ -21,8 +21,8 @@ class BaseOAuthClient:
|
||||||
|
|
||||||
session: Session = None
|
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)
|
|
@ -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",
|
|
@ -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",),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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")
|
|
@ -0,0 +1,15 @@
|
||||||
|
"""Oauth2 Client Settings"""
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
"passbook.channels.in_oauth.backends.AuthorizedServiceBackend",
|
||||||
|
]
|
||||||
|
|
||||||
|
PASSBOOK_SOURCES_OAUTH_TYPES = [
|
||||||
|
"passbook.channels.in_oauth.types.discord",
|
||||||
|
"passbook.channels.in_oauth.types.facebook",
|
||||||
|
"passbook.channels.in_oauth.types.github",
|
||||||
|
"passbook.channels.in_oauth.types.google",
|
||||||
|
"passbook.channels.in_oauth.types.reddit",
|
||||||
|
"passbook.channels.in_oauth.types.twitter",
|
||||||
|
"passbook.channels.in_oauth.types.azure_ad",
|
||||||
|
]
|
|
@ -9,12 +9,12 @@
|
||||||
<div class="pf-c-card__body">
|
<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 %}
|
|
@ -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],
|
|
@ -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"),
|
|
@ -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", ""),
|
|
@ -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", ""),
|
|
@ -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", ""),
|
|
@ -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()
|
|
@ -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,
|
|
@ -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", ""),
|
|
@ -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",
|
||||||
),
|
),
|
|
@ -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,},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""Passbook SAML app config"""
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PassbookInletSAMLConfig(AppConfig):
|
||||||
|
"""passbook saml_idp app config"""
|
||||||
|
|
||||||
|
name = "passbook.channels.in_saml"
|
||||||
|
label = "passbook_channels_in_saml"
|
||||||
|
verbose_name = "passbook Inlets.SAML"
|
||||||
|
mountpoint = "source/saml/"
|
|
@ -4,17 +4,17 @@ from django import forms
|
||||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
from django.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",
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-05-15 19:59
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_crypto", "0001_initial"),
|
||||||
|
("passbook_core", "__first__"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SAMLInlet",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"inlet_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="passbook_core.Inlet",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"issuer",
|
||||||
|
models.TextField(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
help_text="Also known as Entity ID. Defaults the Metadata URL.",
|
||||||
|
verbose_name="Issuer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("idp_url", models.URLField(verbose_name="IDP URL")),
|
||||||
|
(
|
||||||
|
"idp_logout_url",
|
||||||
|
models.URLField(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
verbose_name="IDP Logout URL",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("auto_logout", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"signing_kp",
|
||||||
|
models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
help_text="Certificate Key Pair of the IdP which Assertions are validated against.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="passbook_crypto.CertificateKeyPair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "SAML Inlet",
|
||||||
|
"verbose_name_plural": "SAML Inlets",
|
||||||
|
},
|
||||||
|
bases=("passbook_core.inlet",),
|
||||||
|
),
|
||||||
|
]
|
|
@ -3,13 +3,13 @@ from django.db import models
|
||||||
from django.urls import reverse_lazy
|
from django.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")
|
|
@ -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]:
|
||||||
"""
|
"""
|
|
@ -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"),
|
|
@ -0,0 +1,20 @@
|
||||||
|
"""saml sp helpers"""
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
|
||||||
|
from passbook.channels.in_saml.models import SAMLInlet
|
||||||
|
|
||||||
|
|
||||||
|
def get_issuer(request: HttpRequest, inlet: SAMLInlet) -> str:
|
||||||
|
"""Get Inlet's Issuer, falling back to our Metadata URL if none is set"""
|
||||||
|
issuer = inlet.issuer
|
||||||
|
if issuer is None:
|
||||||
|
return build_full_url("metadata", request, inlet)
|
||||||
|
return issuer
|
||||||
|
|
||||||
|
|
||||||
|
def build_full_url(view: str, request: HttpRequest, inlet: SAMLInlet) -> str:
|
||||||
|
"""Build Full ACS URL to be used in IDP"""
|
||||||
|
return request.build_absolute_uri(
|
||||||
|
reverse(f"passbook_channels_in_saml:{view}", kwargs={"inlet_slug": inlet.slug})
|
||||||
|
)
|
|
@ -7,36 +7,36 @@ from django.views import View
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from 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,
|
||||||
},
|
},
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
@ -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/"
|
|
@ -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(),
|
|
@ -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",),
|
||||||
),
|
),
|
||||||
]
|
]
|
|
@ -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")
|
|
@ -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"
|
|
@ -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(
|
|
@ -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__,
|
||||||
},
|
},
|
|
@ -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
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""passbook auth oauth provider app config"""
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PassbookOutletOAuthConfig(AppConfig):
|
||||||
|
"""passbook auth oauth provider app config"""
|
||||||
|
|
||||||
|
name = "passbook.channels.out_oauth"
|
||||||
|
label = "passbook_channels_out_oauth"
|
||||||
|
verbose_name = "passbook Outlets.OAuth"
|
||||||
|
mountpoint = ""
|
|
@ -1,16 +1,16 @@
|
||||||
"""passbook OAuth2 Provider Forms"""
|
"""passbook OAuth2 Outlet Forms"""
|
||||||
|
|
||||||
from django import forms
|
from 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",
|
|
@ -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),
|
||||||
),
|
),
|
||||||
]
|
]
|
|
@ -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")
|
|
@ -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
Reference in New Issue