*: 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,
|
||||
flows,
|
||||
groups,
|
||||
inlets,
|
||||
invitations,
|
||||
outlets,
|
||||
overview,
|
||||
policy,
|
||||
policies,
|
||||
property_mapping,
|
||||
providers,
|
||||
sources,
|
||||
stages,
|
||||
users,
|
||||
)
|
||||
|
@ -39,51 +39,49 @@ urlpatterns = [
|
|||
applications.ApplicationDeleteView.as_view(),
|
||||
name="application-delete",
|
||||
),
|
||||
# Sources
|
||||
path("sources/", sources.SourceListView.as_view(), name="sources"),
|
||||
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
|
||||
# Inlets
|
||||
path("inlets/", inlets.InletListView.as_view(), name="inlets"),
|
||||
path("inlets/create/", inlets.InletCreateView.as_view(), name="inlet-create"),
|
||||
path(
|
||||
"sources/<uuid:pk>/update/",
|
||||
sources.SourceUpdateView.as_view(),
|
||||
name="source-update",
|
||||
"inlets/<uuid:pk>/update/",
|
||||
inlets.InletUpdateView.as_view(),
|
||||
name="inlet-update",
|
||||
),
|
||||
path(
|
||||
"sources/<uuid:pk>/delete/",
|
||||
sources.SourceDeleteView.as_view(),
|
||||
name="source-delete",
|
||||
"inlets/<uuid:pk>/delete/",
|
||||
inlets.InletDeleteView.as_view(),
|
||||
name="inlet-delete",
|
||||
),
|
||||
# Policies
|
||||
path("policies/", policy.PolicyListView.as_view(), name="policies"),
|
||||
path("policies/create/", policy.PolicyCreateView.as_view(), name="policy-create"),
|
||||
path("policies/", policies.PolicyListView.as_view(), name="policies"),
|
||||
path("policies/create/", policies.PolicyCreateView.as_view(), name="policy-create"),
|
||||
path(
|
||||
"policies/<uuid:pk>/update/",
|
||||
policy.PolicyUpdateView.as_view(),
|
||||
policies.PolicyUpdateView.as_view(),
|
||||
name="policy-update",
|
||||
),
|
||||
path(
|
||||
"policies/<uuid:pk>/delete/",
|
||||
policy.PolicyDeleteView.as_view(),
|
||||
policies.PolicyDeleteView.as_view(),
|
||||
name="policy-delete",
|
||||
),
|
||||
path(
|
||||
"policies/<uuid:pk>/test/", policy.PolicyTestView.as_view(), name="policy-test"
|
||||
"policies/<uuid:pk>/test/",
|
||||
policies.PolicyTestView.as_view(),
|
||||
name="policy-test",
|
||||
),
|
||||
# Providers
|
||||
path("providers/", providers.ProviderListView.as_view(), name="providers"),
|
||||
# Outlets
|
||||
path("outlets/", outlets.OutletListView.as_view(), name="outlets"),
|
||||
path("outlets/create/", outlets.OutletCreateView.as_view(), name="outlet-create",),
|
||||
path(
|
||||
"providers/create/",
|
||||
providers.ProviderCreateView.as_view(),
|
||||
name="provider-create",
|
||||
"outlets/<int:pk>/update/",
|
||||
outlets.OutletUpdateView.as_view(),
|
||||
name="outlet-update",
|
||||
),
|
||||
path(
|
||||
"providers/<int:pk>/update/",
|
||||
providers.ProviderUpdateView.as_view(),
|
||||
name="provider-update",
|
||||
),
|
||||
path(
|
||||
"providers/<int:pk>/delete/",
|
||||
providers.ProviderDeleteView.as_view(),
|
||||
name="provider-delete",
|
||||
"outlets/<int:pk>/delete/",
|
||||
outlets.OutletDeleteView.as_view(),
|
||||
name="outlet-delete",
|
||||
),
|
||||
# Stages
|
||||
path("stages/", stages.StageListView.as_view(), name="stages"),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""passbook Provider administration"""
|
||||
"""passbook Inlet administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
|
@ -11,23 +11,23 @@ from django.utils.translation import ugettext as _
|
|||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.core.models import Provider
|
||||
from passbook.core.models import Inlet
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all providers"""
|
||||
class InletListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all inlets"""
|
||||
|
||||
model = Provider
|
||||
permission_required = "passbook_core.add_provider"
|
||||
template_name = "administration/provider/list.html"
|
||||
paginate_by = 10
|
||||
ordering = "id"
|
||||
model = Inlet
|
||||
permission_required = "passbook_core.view_inlet"
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
template_name = "administration/inlet/list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {
|
||||
x.__name__: x._meta.verbose_name for x in all_subclasses(Provider)
|
||||
x.__name__: x._meta.verbose_name for x in all_subclasses(Inlet)
|
||||
}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
@ -35,40 +35,40 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
|||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class ProviderCreateView(
|
||||
class InletCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Provider"""
|
||||
"""Create new Inlet"""
|
||||
|
||||
model = Provider
|
||||
permission_required = "passbook_core.add_provider"
|
||||
model = Inlet
|
||||
permission_required = "passbook_core.add_inlet"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully created Provider")
|
||||
success_url = reverse_lazy("passbook_admin:inlets")
|
||||
success_message = _("Successfully created Inlet")
|
||||
|
||||
def get_form_class(self):
|
||||
provider_type = self.request.GET.get("type")
|
||||
model = next(x for x in all_subclasses(Provider) if x.__name__ == provider_type)
|
||||
inlet_type = self.request.GET.get("type")
|
||||
model = next(x for x in all_subclasses(Inlet) if x.__name__ == inlet_type)
|
||||
if not model:
|
||||
raise Http404
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class ProviderUpdateView(
|
||||
class InletUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update provider"""
|
||||
"""Update inlet"""
|
||||
|
||||
model = Provider
|
||||
permission_required = "passbook_core.change_provider"
|
||||
model = Inlet
|
||||
permission_required = "passbook_core.change_inlet"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully updated Provider")
|
||||
success_url = reverse_lazy("passbook_admin:inlets")
|
||||
success_message = _("Successfully updated Inlet")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
|
@ -77,29 +77,25 @@ class ProviderUpdateView(
|
|||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Provider.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
Inlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class ProviderDeleteView(
|
||||
class InletDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete provider"""
|
||||
"""Delete inlet"""
|
||||
|
||||
model = Provider
|
||||
permission_required = "passbook_core.delete_provider"
|
||||
model = Inlet
|
||||
permission_required = "passbook_core.delete_inlet"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully deleted Provider")
|
||||
success_url = reverse_lazy("passbook_admin:inlets")
|
||||
success_message = _("Successfully deleted Inlet")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Provider.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
Inlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
|
@ -1,4 +1,4 @@
|
|||
"""passbook Source administration"""
|
||||
"""passbook Outlet administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
|
@ -11,23 +11,23 @@ from django.utils.translation import ugettext as _
|
|||
from django.views.generic import DeleteView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.core.models import Source
|
||||
from passbook.core.models import Outlet
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all sources"""
|
||||
class OutletListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all outlets"""
|
||||
|
||||
model = Source
|
||||
permission_required = "passbook_core.view_source"
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
template_name = "administration/source/list.html"
|
||||
model = Outlet
|
||||
permission_required = "passbook_core.add_outlet"
|
||||
template_name = "administration/outlet/list.html"
|
||||
paginate_by = 10
|
||||
ordering = "id"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["types"] = {
|
||||
x.__name__: x._meta.verbose_name for x in all_subclasses(Source)
|
||||
x.__name__: x._meta.verbose_name for x in all_subclasses(Outlet)
|
||||
}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
@ -35,40 +35,40 @@ class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
|||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class SourceCreateView(
|
||||
class OutletCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Source"""
|
||||
"""Create new Outlet"""
|
||||
|
||||
model = Source
|
||||
permission_required = "passbook_core.add_source"
|
||||
model = Outlet
|
||||
permission_required = "passbook_core.add_outlet"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully created Source")
|
||||
success_url = reverse_lazy("passbook_admin:outlets")
|
||||
success_message = _("Successfully created Outlet")
|
||||
|
||||
def get_form_class(self):
|
||||
source_type = self.request.GET.get("type")
|
||||
model = next(x for x in all_subclasses(Source) if x.__name__ == source_type)
|
||||
outlet_type = self.request.GET.get("type")
|
||||
model = next(x for x in all_subclasses(Outlet) if x.__name__ == outlet_type)
|
||||
if not model:
|
||||
raise Http404
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class SourceUpdateView(
|
||||
class OutletUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update source"""
|
||||
"""Update outlet"""
|
||||
|
||||
model = Source
|
||||
permission_required = "passbook_core.change_source"
|
||||
model = Outlet
|
||||
permission_required = "passbook_core.change_outlet"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully updated Source")
|
||||
success_url = reverse_lazy("passbook_admin:outlets")
|
||||
success_message = _("Successfully updated Outlet")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
|
@ -77,25 +77,25 @@ class SourceUpdateView(
|
|||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
Outlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class SourceDeleteView(
|
||||
class OutletDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete source"""
|
||||
"""Delete outlet"""
|
||||
|
||||
model = Source
|
||||
permission_required = "passbook_core.delete_source"
|
||||
model = Outlet
|
||||
permission_required = "passbook_core.delete_outlet"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully deleted Source")
|
||||
success_url = reverse_lazy("passbook_admin:outlets")
|
||||
success_message = _("Successfully deleted Outlet")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return (
|
||||
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
Outlet.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
|
@ -5,8 +5,9 @@ from django.views.generic import TemplateView
|
|||
|
||||
from passbook import __version__
|
||||
from passbook.admin.mixins import AdminRequiredMixin
|
||||
from passbook.core.models import Application, Policy, Provider, Source, User
|
||||
from passbook.core.models import Application, Inlet, Outlet, User
|
||||
from passbook.flows.models import Flow, Stage
|
||||
from passbook.policies.models import Policy
|
||||
from passbook.root.celery import CELERY_APP
|
||||
from passbook.stages.invitation.models import Invitation
|
||||
|
||||
|
@ -27,16 +28,14 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
|||
kwargs["application_count"] = len(Application.objects.all())
|
||||
kwargs["policy_count"] = len(Policy.objects.all())
|
||||
kwargs["user_count"] = len(User.objects.all())
|
||||
kwargs["provider_count"] = len(Provider.objects.all())
|
||||
kwargs["source_count"] = len(Source.objects.all())
|
||||
kwargs["outlet_count"] = len(Outlet.objects.all())
|
||||
kwargs["inlet_count"] = len(Inlet.objects.all())
|
||||
kwargs["stage_count"] = len(Stage.objects.all())
|
||||
kwargs["flow_count"] = len(Flow.objects.all())
|
||||
kwargs["invitation_count"] = len(Invitation.objects.all())
|
||||
kwargs["version"] = __version__
|
||||
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
|
||||
kwargs["providers_without_application"] = Provider.objects.filter(
|
||||
application=None
|
||||
)
|
||||
kwargs["outlets_without_application"] = Outlet.objects.filter(application=None)
|
||||
kwargs["policies_without_binding"] = len(
|
||||
Policy.objects.filter(policymodel__isnull=True)
|
||||
)
|
||||
|
|
|
@ -13,10 +13,10 @@ from django.views.generic.detail import DetailView
|
|||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.forms.policies import PolicyTestForm
|
||||
from passbook.core.models import Policy
|
||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
from passbook.policies.models import Policy
|
||||
|
||||
|
||||
class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
|
@ -16,7 +16,7 @@ from guardian.mixins import (
|
|||
)
|
||||
|
||||
from passbook.admin.forms.users import UserForm
|
||||
from passbook.core.models import Nonce, User
|
||||
from passbook.core.models import Token, User
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
|
@ -92,12 +92,12 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
|||
permission_required = "passbook_core.reset_user_password"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Create nonce for user and return link"""
|
||||
"""Create token for user and return link"""
|
||||
super().get(request, *args, **kwargs)
|
||||
# TODO: create plan for user, get token
|
||||
nonce = Nonce.objects.create(user=self.object)
|
||||
token = Token.objects.create(user=self.object)
|
||||
link = request.build_absolute_uri(
|
||||
reverse("passbook_flows:default-recovery", kwargs={"nonce": nonce.uuid})
|
||||
reverse("passbook_flows:default-recovery", kwargs={"token": token.uuid})
|
||||
)
|
||||
messages.success(
|
||||
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
"""permission classes for django restframework"""
|
||||
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
|
||||
|
||||
from passbook.core.models import PolicyModel
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
from passbook.policies.models import PolicyBindingModel
|
||||
|
||||
|
||||
class CustomObjectPermissions(DjangoObjectPermissions):
|
||||
|
@ -24,8 +24,7 @@ class PolicyPermissions(BasePermission):
|
|||
|
||||
policy_engine: PolicyEngine
|
||||
|
||||
def has_object_permission(self, request, view, obj: PolicyModel) -> bool:
|
||||
# if not obj.po
|
||||
self.policy_engine = PolicyEngine(obj.policies, request.user, request)
|
||||
def has_object_permission(self, request, view, obj: PolicyBindingModel) -> bool:
|
||||
self.policy_engine = PolicyEngine(obj.policies.all(), request.user, request)
|
||||
self.policy_engine.request.obj = obj
|
||||
return self.policy_engine.build().passing
|
||||
|
|
|
@ -9,12 +9,18 @@ from structlog import get_logger
|
|||
|
||||
from passbook.api.permissions import CustomObjectPermissions
|
||||
from passbook.audit.api import EventViewSet
|
||||
from passbook.channels.in_ldap.api import LDAPInletViewSet, LDAPPropertyMappingViewSet
|
||||
from passbook.channels.in_oauth.api import OAuthInletViewSet
|
||||
from passbook.channels.out_app_gw.api import ApplicationGatewayOutletViewSet
|
||||
from passbook.channels.out_oauth.api import OAuth2OutletViewSet
|
||||
from passbook.channels.out_oidc.api import OpenIDOutletViewSet
|
||||
from passbook.channels.out_saml.api import SAMLOutletViewSet, SAMLPropertyMappingViewSet
|
||||
from passbook.core.api.applications import ApplicationViewSet
|
||||
from passbook.core.api.groups import GroupViewSet
|
||||
from passbook.core.api.inlets import InletViewSet
|
||||
from passbook.core.api.outlets import OutletViewSet
|
||||
from passbook.core.api.policies import PolicyViewSet
|
||||
from passbook.core.api.propertymappings import PropertyMappingViewSet
|
||||
from passbook.core.api.providers import ProviderViewSet
|
||||
from passbook.core.api.sources import SourceViewSet
|
||||
from passbook.core.api.users import UserViewSet
|
||||
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
|
||||
from passbook.lib.utils.reflection import get_apps
|
||||
|
@ -24,12 +30,6 @@ from passbook.policies.expression.api import ExpressionPolicyViewSet
|
|||
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
||||
from passbook.policies.password.api import PasswordPolicyViewSet
|
||||
from passbook.policies.reputation.api import ReputationPolicyViewSet
|
||||
from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet
|
||||
from passbook.providers.oauth.api import OAuth2ProviderViewSet
|
||||
from passbook.providers.oidc.api import OpenIDProviderViewSet
|
||||
from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
|
||||
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||
from passbook.sources.oauth.api import OAuthSourceViewSet
|
||||
from passbook.stages.captcha.api import CaptchaStageViewSet
|
||||
from passbook.stages.email.api import EmailStageViewSet
|
||||
from passbook.stages.identification.api import IdentificationStageViewSet
|
||||
|
@ -57,9 +57,15 @@ router.register("core/users", UserViewSet)
|
|||
|
||||
router.register("audit/events", EventViewSet)
|
||||
|
||||
router.register("sources/all", SourceViewSet)
|
||||
router.register("sources/ldap", LDAPSourceViewSet)
|
||||
router.register("sources/oauth", OAuthSourceViewSet)
|
||||
router.register("inlets/all", InletViewSet)
|
||||
router.register("inlets/ldap", LDAPInletViewSet)
|
||||
router.register("inlets/oauth", OAuthInletViewSet)
|
||||
|
||||
router.register("outlets/all", OutletViewSet)
|
||||
router.register("outlets/applicationgateway", ApplicationGatewayOutletViewSet)
|
||||
router.register("outlets/oauth", OAuth2OutletViewSet)
|
||||
router.register("outlets/openid", OpenIDOutletViewSet)
|
||||
router.register("outlets/saml", SAMLOutletViewSet)
|
||||
|
||||
router.register("policies/all", PolicyViewSet)
|
||||
router.register("policies/bindings", PolicyBindingViewSet)
|
||||
|
@ -69,12 +75,6 @@ router.register("policies/password", PasswordPolicyViewSet)
|
|||
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
|
||||
router.register("policies/reputation", ReputationPolicyViewSet)
|
||||
|
||||
router.register("providers/all", ProviderViewSet)
|
||||
router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
|
||||
router.register("providers/oauth", OAuth2ProviderViewSet)
|
||||
router.register("providers/openid", OpenIDProviderViewSet)
|
||||
router.register("providers/saml", SAMLProviderViewSet)
|
||||
|
||||
router.register("propertymappings/all", PropertyMappingViewSet)
|
||||
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 2.2.6 on 2019-10-07 14:07
|
||||
# Generated by Django 3.0.5 on 2020-05-15 19:58
|
||||
|
||||
import uuid
|
||||
|
||||
|
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
|||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AuditEntry",
|
||||
name="Event",
|
||||
fields=[
|
||||
(
|
||||
"uuid",
|
||||
|
@ -33,15 +33,16 @@ class Migration(migrations.Migration):
|
|||
"action",
|
||||
models.TextField(
|
||||
choices=[
|
||||
("login", "login"),
|
||||
("login_failed", "login_failed"),
|
||||
("logout", "logout"),
|
||||
("authorize_application", "authorize_application"),
|
||||
("suspicious_request", "suspicious_request"),
|
||||
("sign_up", "sign_up"),
|
||||
("password_reset", "password_reset"),
|
||||
("invitation_created", "invitation_created"),
|
||||
("invitation_used", "invitation_used"),
|
||||
("LOGIN", "login"),
|
||||
("LOGIN_FAILED", "login_failed"),
|
||||
("LOGOUT", "logout"),
|
||||
("AUTHORIZE_APPLICATION", "authorize_application"),
|
||||
("SUSPICIOUS_REQUEST", "suspicious_request"),
|
||||
("SIGN_UP", "sign_up"),
|
||||
("PASSWORD_RESET", "password_reset"),
|
||||
("INVITE_CREATED", "invitation_created"),
|
||||
("INVITE_USED", "invitation_used"),
|
||||
("CUSTOM", "custom"),
|
||||
]
|
||||
),
|
||||
),
|
||||
|
@ -53,7 +54,7 @@ class Migration(migrations.Migration):
|
|||
blank=True, default=dict
|
||||
),
|
||||
),
|
||||
("request_ip", models.GenericIPAddressField()),
|
||||
("client_ip", models.GenericIPAddressField(null=True)),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"user",
|
||||
|
@ -65,8 +66,8 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Audit Entry",
|
||||
"verbose_name_plural": "Audit Entries",
|
||||
"verbose_name": "Audit Event",
|
||||
"verbose_name_plural": "Audit Events",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
# Generated by Django 2.2.6 on 2019-10-28 08:29
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("passbook_audit", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(old_name="AuditEntry", new_name="Event",),
|
||||
]
|
|
@ -1,40 +0,0 @@
|
|||
# Generated by Django 2.2.8 on 2019-12-05 14:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import passbook.audit.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_audit", "0002_auto_20191028_0829"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="event",
|
||||
options={
|
||||
"verbose_name": "Audit Event",
|
||||
"verbose_name_plural": "Audit Events",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("LOGIN", "login"),
|
||||
("LOGIN_FAILED", "login_failed"),
|
||||
("LOGOUT", "logout"),
|
||||
("AUTHORIZE_APPLICATION", "authorize_application"),
|
||||
("SUSPICIOUS_REQUEST", "suspicious_request"),
|
||||
("SIGN_UP", "sign_up"),
|
||||
("PASSWORD_RESET", "password_reset"),
|
||||
("INVITE_CREATED", "invitation_created"),
|
||||
("INVITE_USED", "invitation_used"),
|
||||
("CUSTOM", "custom"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 2.2.8 on 2019-12-05 15:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_audit", "0003_auto_20191205_1407"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(model_name="event", name="request_ip",),
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="client_ip",
|
||||
field=models.GenericIPAddressField(null=True),
|
||||
),
|
||||
]
|
|
@ -5,7 +5,7 @@ from django.test import TestCase
|
|||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.models import Policy
|
||||
|
||||
|
||||
class TestAuditEvent(TestCase):
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
"""Source API Views"""
|
||||
"""Inlet API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.admin.forms.source import SOURCE_SERIALIZER_FIELDS
|
||||
from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
from passbook.admin.forms.inlet import INLET_SERIALIZER_FIELDS
|
||||
from passbook.channels.in_ldap.models import LDAPInlet, LDAPPropertyMapping
|
||||
|
||||
|
||||
class LDAPSourceSerializer(ModelSerializer):
|
||||
"""LDAP Source Serializer"""
|
||||
class LDAPInletSerializer(ModelSerializer):
|
||||
"""LDAP Inlet Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = LDAPSource
|
||||
fields = SOURCE_SERIALIZER_FIELDS + [
|
||||
model = LDAPInlet
|
||||
fields = INLET_SERIALIZER_FIELDS + [
|
||||
"server_uri",
|
||||
"bind_cn",
|
||||
"bind_password",
|
||||
|
@ -38,11 +38,11 @@ class LDAPPropertyMappingSerializer(ModelSerializer):
|
|||
fields = ["pk", "name", "expression", "object_field"]
|
||||
|
||||
|
||||
class LDAPSourceViewSet(ModelViewSet):
|
||||
"""LDAP Source Viewset"""
|
||||
class LDAPInletViewSet(ModelViewSet):
|
||||
"""LDAP Inlet Viewset"""
|
||||
|
||||
queryset = LDAPSource.objects.all()
|
||||
serializer_class = LDAPSourceSerializer
|
||||
queryset = LDAPInlet.objects.all()
|
||||
serializer_class = LDAPInletSerializer
|
||||
|
||||
|
||||
class LDAPPropertyMappingViewSet(ModelViewSet):
|
|
@ -0,0 +1,11 @@
|
|||
"""Passbook ldap app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookInletLDAPConfig(AppConfig):
|
||||
"""Passbook ldap app config"""
|
||||
|
||||
name = "passbook.channels.in_ldap"
|
||||
label = "passbook_channels_in_ldap"
|
||||
verbose_name = "passbook Inlets.LDAP"
|
|
@ -3,8 +3,8 @@ from django.contrib.auth.backends import ModelBackend
|
|||
from django.http import HttpRequest
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.sources.ldap.connector import Connector
|
||||
from passbook.sources.ldap.models import LDAPSource
|
||||
from passbook.channels.in_ldap.connector import Connector
|
||||
from passbook.channels.in_ldap.models import LDAPInlet
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -16,9 +16,9 @@ class LDAPBackend(ModelBackend):
|
|||
"""Try to authenticate a user via ldap"""
|
||||
if "password" not in kwargs:
|
||||
return None
|
||||
for source in LDAPSource.objects.filter(enabled=True):
|
||||
LOGGER.debug("LDAP Auth attempt", source=source)
|
||||
_ldap = Connector(source)
|
||||
for inlet in LDAPInlet.objects.filter(enabled=True):
|
||||
LOGGER.debug("LDAP Auth attempt", inlet=inlet)
|
||||
_ldap = Connector(inlet)
|
||||
user = _ldap.auth_user(**kwargs)
|
||||
if user:
|
||||
return user
|
|
@ -6,9 +6,9 @@ import ldap3.core.exceptions
|
|||
from django.db.utils import IntegrityError
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.channels.in_ldap.models import LDAPInlet, LDAPPropertyMapping
|
||||
from passbook.core.exceptions import PropertyMappingExpressionException
|
||||
from passbook.core.models import Group, User
|
||||
from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -18,23 +18,23 @@ class Connector:
|
|||
|
||||
_server: ldap3.Server
|
||||
_connection = ldap3.Connection
|
||||
_source: LDAPSource
|
||||
_inlet: LDAPInlet
|
||||
|
||||
def __init__(self, source: LDAPSource):
|
||||
self._source = source
|
||||
def __init__(self, source: LDAPInlet):
|
||||
self._inlet = source
|
||||
self._server = ldap3.Server(source.server_uri) # Implement URI parsing
|
||||
|
||||
def bind(self):
|
||||
"""Bind using Source's Credentials"""
|
||||
"""Bind using Inlet's Credentials"""
|
||||
self._connection = ldap3.Connection(
|
||||
self._server,
|
||||
raise_exceptions=True,
|
||||
user=self._source.bind_cn,
|
||||
password=self._source.bind_password,
|
||||
user=self._inlet.bind_cn,
|
||||
password=self._inlet.bind_password,
|
||||
)
|
||||
|
||||
self._connection.bind()
|
||||
if self._source.start_tls:
|
||||
if self._inlet.start_tls:
|
||||
self._connection.start_tls()
|
||||
|
||||
@staticmethod
|
||||
|
@ -45,21 +45,21 @@ class Connector:
|
|||
@property
|
||||
def base_dn_users(self) -> str:
|
||||
"""Shortcut to get full base_dn for user lookups"""
|
||||
return ",".join([self._source.additional_user_dn, self._source.base_dn])
|
||||
return ",".join([self._inlet.additional_user_dn, self._inlet.base_dn])
|
||||
|
||||
@property
|
||||
def base_dn_groups(self) -> str:
|
||||
"""Shortcut to get full base_dn for group lookups"""
|
||||
return ",".join([self._source.additional_group_dn, self._source.base_dn])
|
||||
return ",".join([self._inlet.additional_group_dn, self._inlet.base_dn])
|
||||
|
||||
def sync_groups(self):
|
||||
"""Iterate over all LDAP Groups and create passbook_core.Group instances"""
|
||||
if not self._source.sync_groups:
|
||||
LOGGER.debug("Group syncing is disabled for this Source")
|
||||
if not self._inlet.sync_groups:
|
||||
LOGGER.debug("Group syncing is disabled for this Inlet")
|
||||
return
|
||||
groups = self._connection.extend.standard.paged_search(
|
||||
search_base=self.base_dn_groups,
|
||||
search_filter=self._source.group_object_filter,
|
||||
search_filter=self._inlet.group_object_filter,
|
||||
search_scope=ldap3.SUBTREE,
|
||||
attributes=ldap3.ALL_ATTRIBUTES,
|
||||
)
|
||||
|
@ -67,15 +67,15 @@ class Connector:
|
|||
attributes = group.get("attributes", {})
|
||||
_, created = Group.objects.update_or_create(
|
||||
attributes__ldap_uniq=attributes.get(
|
||||
self._source.object_uniqueness_field, ""
|
||||
self._inlet.object_uniqueness_field, ""
|
||||
),
|
||||
parent=self._source.sync_parent_group,
|
||||
parent=self._inlet.sync_parent_group,
|
||||
# defaults=self._build_object_properties(attributes),
|
||||
defaults={
|
||||
"name": attributes.get("name", ""),
|
||||
"attributes": {
|
||||
"ldap_uniq": attributes.get(
|
||||
self._source.object_uniqueness_field, ""
|
||||
self._inlet.object_uniqueness_field, ""
|
||||
),
|
||||
"distinguishedName": attributes.get("distinguishedName"),
|
||||
},
|
||||
|
@ -89,14 +89,14 @@ class Connector:
|
|||
"""Iterate over all LDAP Users and create passbook_core.User instances"""
|
||||
users = self._connection.extend.standard.paged_search(
|
||||
search_base=self.base_dn_users,
|
||||
search_filter=self._source.user_object_filter,
|
||||
search_filter=self._inlet.user_object_filter,
|
||||
search_scope=ldap3.SUBTREE,
|
||||
attributes=ldap3.ALL_ATTRIBUTES,
|
||||
)
|
||||
for user in users:
|
||||
attributes = user.get("attributes", {})
|
||||
try:
|
||||
uniq = attributes[self._source.object_uniqueness_field]
|
||||
uniq = attributes[self._inlet.object_uniqueness_field]
|
||||
except KeyError:
|
||||
LOGGER.warning("Cannot find uniqueness Field in attributes")
|
||||
continue
|
||||
|
@ -125,20 +125,20 @@ class Connector:
|
|||
"""Iterate over all Users and assign Groups using memberOf Field"""
|
||||
users = self._connection.extend.standard.paged_search(
|
||||
search_base=self.base_dn_users,
|
||||
search_filter=self._source.user_object_filter,
|
||||
search_filter=self._inlet.user_object_filter,
|
||||
search_scope=ldap3.SUBTREE,
|
||||
attributes=[
|
||||
self._source.user_group_membership_field,
|
||||
self._source.object_uniqueness_field,
|
||||
self._inlet.user_group_membership_field,
|
||||
self._inlet.object_uniqueness_field,
|
||||
],
|
||||
)
|
||||
group_cache: Dict[str, Group] = {}
|
||||
for user in users:
|
||||
member_of = user.get("attributes", {}).get(
|
||||
self._source.user_group_membership_field, []
|
||||
self._inlet.user_group_membership_field, []
|
||||
)
|
||||
uniq = user.get("attributes", {}).get(
|
||||
self._source.object_uniqueness_field, []
|
||||
self._inlet.object_uniqueness_field, []
|
||||
)
|
||||
for group_dn in member_of:
|
||||
# Check if group_dn is within our base_dn_groups, and skip if not
|
||||
|
@ -168,7 +168,7 @@ class Connector:
|
|||
self, attributes: Dict[str, Any]
|
||||
) -> Dict[str, Dict[Any, Any]]:
|
||||
properties = {"attributes": {}}
|
||||
for mapping in self._source.property_mappings.all().select_subclasses():
|
||||
for mapping in self._inlet.property_mappings.all().select_subclasses():
|
||||
if not isinstance(mapping, LDAPPropertyMapping):
|
||||
continue
|
||||
mapping: LDAPPropertyMapping
|
||||
|
@ -179,9 +179,9 @@ class Connector:
|
|||
except PropertyMappingExpressionException as exc:
|
||||
LOGGER.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
|
||||
continue
|
||||
if self._source.object_uniqueness_field in attributes:
|
||||
if self._inlet.object_uniqueness_field in attributes:
|
||||
properties["attributes"]["ldap_uniq"] = attributes.get(
|
||||
self._source.object_uniqueness_field
|
||||
self._inlet.object_uniqueness_field
|
||||
)
|
||||
properties["attributes"]["distinguishedName"] = attributes.get(
|
||||
"distinguishedName"
|
|
@ -4,17 +4,17 @@ from django import forms
|
|||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
|
||||
from passbook.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
from passbook.admin.forms.inlet import INLET_FORM_FIELDS
|
||||
from passbook.channels.in_ldap.models import LDAPInlet, LDAPPropertyMapping
|
||||
|
||||
|
||||
class LDAPSourceForm(forms.ModelForm):
|
||||
"""LDAPSource Form"""
|
||||
class LDAPInletForm(forms.ModelForm):
|
||||
"""LDAPInlet Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = LDAPSource
|
||||
fields = SOURCE_FORM_FIELDS + [
|
||||
model = LDAPInlet
|
||||
fields = INLET_FORM_FIELDS + [
|
||||
"server_uri",
|
||||
"bind_cn",
|
||||
"bind_password",
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 2.2.6 on 2019-10-08 20:43
|
||||
# Generated by Django 3.0.5 on 2020-05-15 19:59
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
|
@ -10,7 +10,7 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0001_initial"),
|
||||
("passbook_core", "__first__"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -28,69 +28,104 @@ class Migration(migrations.Migration):
|
|||
to="passbook_core.PropertyMapping",
|
||||
),
|
||||
),
|
||||
("ldap_property", models.TextField()),
|
||||
("object_field", models.TextField()),
|
||||
],
|
||||
options={"abstract": False,},
|
||||
options={
|
||||
"verbose_name": "LDAP Property Mapping",
|
||||
"verbose_name_plural": "LDAP Property Mappings",
|
||||
},
|
||||
bases=("passbook_core.propertymapping",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LDAPSource",
|
||||
name="LDAPInlet",
|
||||
fields=[
|
||||
(
|
||||
"source_ptr",
|
||||
"inlet_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_core.Source",
|
||||
to="passbook_core.Inlet",
|
||||
),
|
||||
),
|
||||
(
|
||||
"server_uri",
|
||||
models.URLField(
|
||||
models.TextField(
|
||||
validators=[
|
||||
django.core.validators.URLValidator(
|
||||
schemes=["ldap", "ldaps"]
|
||||
)
|
||||
]
|
||||
],
|
||||
verbose_name="Server URI",
|
||||
),
|
||||
),
|
||||
("bind_cn", models.TextField()),
|
||||
("bind_cn", models.TextField(verbose_name="Bind CN")),
|
||||
("bind_password", models.TextField()),
|
||||
("start_tls", models.BooleanField(default=False)),
|
||||
("base_dn", models.TextField()),
|
||||
(
|
||||
"start_tls",
|
||||
models.BooleanField(default=False, verbose_name="Enable Start TLS"),
|
||||
),
|
||||
("base_dn", models.TextField(verbose_name="Base DN")),
|
||||
(
|
||||
"additional_user_dn",
|
||||
models.TextField(
|
||||
help_text="Prepended to Base DN for User-queries."
|
||||
help_text="Prepended to Base DN for User-queries.",
|
||||
verbose_name="Addition User DN",
|
||||
),
|
||||
),
|
||||
(
|
||||
"additional_group_dn",
|
||||
models.TextField(
|
||||
help_text="Prepended to Base DN for Group-queries."
|
||||
help_text="Prepended to Base DN for Group-queries.",
|
||||
verbose_name="Addition Group DN",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_object_filter",
|
||||
models.TextField(
|
||||
default="(objectCategory=Person)",
|
||||
help_text="Consider Objects matching this filter to be Users.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_group_membership_field",
|
||||
models.TextField(
|
||||
default="memberOf",
|
||||
help_text="Field which contains Groups of user.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"group_object_filter",
|
||||
models.TextField(
|
||||
default="(objectCategory=Group)",
|
||||
help_text="Consider Objects matching this filter to be Groups.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"object_uniqueness_field",
|
||||
models.TextField(
|
||||
default="objectSid",
|
||||
help_text="Field which contains a unique Identifier.",
|
||||
),
|
||||
),
|
||||
("user_object_filter", models.TextField()),
|
||||
("group_object_filter", models.TextField()),
|
||||
("sync_groups", models.BooleanField(default=True)),
|
||||
(
|
||||
"sync_parent_group",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="passbook_core.Group",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "LDAP Source",
|
||||
"verbose_name_plural": "LDAP Sources",
|
||||
"verbose_name": "LDAP Inlet",
|
||||
"verbose_name_plural": "LDAP Inlets",
|
||||
},
|
||||
bases=("passbook_core.source",),
|
||||
bases=("passbook_core.inlet",),
|
||||
),
|
||||
]
|
|
@ -4,11 +4,11 @@ from django.core.validators import URLValidator
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.core.models import Group, PropertyMapping, Source
|
||||
from passbook.core.models import Group, Inlet, PropertyMapping
|
||||
|
||||
|
||||
class LDAPSource(Source):
|
||||
"""LDAP Authentication source"""
|
||||
class LDAPInlet(Inlet):
|
||||
"""LDAP Authentication inlet"""
|
||||
|
||||
server_uri = models.TextField(
|
||||
validators=[URLValidator(schemes=["ldap", "ldaps"])],
|
||||
|
@ -48,12 +48,12 @@ class LDAPSource(Source):
|
|||
Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT
|
||||
)
|
||||
|
||||
form = "passbook.sources.ldap.forms.LDAPSourceForm"
|
||||
form = "passbook.channels.in_ldap.forms.LDAPInletForm"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("LDAP Source")
|
||||
verbose_name_plural = _("LDAP Sources")
|
||||
verbose_name = _("LDAP Inlet")
|
||||
verbose_name_plural = _("LDAP Inlets")
|
||||
|
||||
|
||||
class LDAPPropertyMapping(PropertyMapping):
|
||||
|
@ -61,7 +61,7 @@ class LDAPPropertyMapping(PropertyMapping):
|
|||
|
||||
object_field = models.TextField()
|
||||
|
||||
form = "passbook.sources.ldap.forms.LDAPPropertyMappingForm"
|
||||
form = "passbook.channels.in_ldap.forms.LDAPPropertyMappingForm"
|
||||
|
||||
def __str__(self):
|
||||
return f"LDAP Property Mapping {self.expression} -> {self.object_field}"
|
|
@ -2,12 +2,12 @@
|
|||
from celery.schedules import crontab
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"passbook.sources.ldap.auth.LDAPBackend",
|
||||
"passbook.channels.in_ldap.auth.LDAPBackend",
|
||||
]
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"sync": {
|
||||
"task": "passbook.sources.ldap.tasks.sync",
|
||||
"task": "passbook.channels.in_ldap.tasks.sync",
|
||||
"schedule": crontab(minute=0), # Run every hour
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
||||
class PassbookSourceOAuthConfig(AppConfig):
|
||||
class PassbookInletOAuthConfig(AppConfig):
|
||||
"""passbook source.oauth config"""
|
||||
|
||||
name = "passbook.sources.oauth"
|
||||
label = "passbook_sources_oauth"
|
||||
verbose_name = "passbook Sources.OAuth"
|
||||
name = "passbook.channels.in_oauth"
|
||||
label = "passbook_channels_in_oauth"
|
||||
verbose_name = "passbook Inlets.OAuth"
|
||||
mountpoint = "source/oauth/"
|
||||
|
||||
def ready(self):
|
|
@ -0,0 +1,24 @@
|
|||
"""passbook oauth_client Authorization backend"""
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.db.models import Q
|
||||
|
||||
from passbook.channels.in_oauth.models import OAuthInlet, UserOAuthInletConnection
|
||||
|
||||
|
||||
class AuthorizedServiceBackend(ModelBackend):
|
||||
"Authentication backend for users registered with remote OAuth provider."
|
||||
|
||||
def authenticate(self, request, inlet=None, identifier=None):
|
||||
"Fetch user for a given inlet by id."
|
||||
inlet_q = Q(inlet__name=inlet)
|
||||
if isinstance(inlet, OAuthInlet):
|
||||
inlet_q = Q(inlet=inlet)
|
||||
try:
|
||||
access = UserOAuthInletConnection.objects.filter(
|
||||
inlet_q, identifier=identifier
|
||||
).select_related("user")[0]
|
||||
except IndexError:
|
||||
return None
|
||||
else:
|
||||
return access.user
|
|
@ -21,8 +21,8 @@ class BaseOAuthClient:
|
|||
|
||||
session: Session = None
|
||||
|
||||
def __init__(self, source, token=""): # nosec
|
||||
self.source = source
|
||||
def __init__(self, inlet, token=""): # nosec
|
||||
self.inlet = inlet
|
||||
self.token = token
|
||||
self.session = Session()
|
||||
self.session.headers.update({"User-Agent": "passbook %s" % __version__})
|
||||
|
@ -38,7 +38,7 @@ class BaseOAuthClient:
|
|||
"Authorization": f"{token['token_type']} {token['access_token']}"
|
||||
}
|
||||
response = self.session.request(
|
||||
"get", self.source.profile_url, headers=headers,
|
||||
"get", self.inlet.profile_url, headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
|
@ -58,7 +58,7 @@ class BaseOAuthClient:
|
|||
args.update(additional)
|
||||
params = urlencode(args)
|
||||
LOGGER.info("redirect args", **args)
|
||||
return "{0}?{1}".format(self.source.authorization_url, params)
|
||||
return "{0}?{1}".format(self.inlet.authorization_url, params)
|
||||
|
||||
def parse_raw_token(self, raw_token):
|
||||
"Parse token and secret from raw token response."
|
||||
|
@ -94,7 +94,7 @@ class OAuthClient(BaseOAuthClient):
|
|||
try:
|
||||
response = self.session.request(
|
||||
"post",
|
||||
self.source.access_token_url,
|
||||
self.inlet.access_token_url,
|
||||
data=data,
|
||||
headers=self._default_headers,
|
||||
)
|
||||
|
@ -112,7 +112,7 @@ class OAuthClient(BaseOAuthClient):
|
|||
try:
|
||||
response = self.session.request(
|
||||
"post",
|
||||
self.source.request_token_url,
|
||||
self.inlet.request_token_url,
|
||||
data={"oauth_callback": callback},
|
||||
headers=self._default_headers,
|
||||
)
|
||||
|
@ -151,10 +151,10 @@ class OAuthClient(BaseOAuthClient):
|
|||
callback = kwargs.pop("oauth_callback", None)
|
||||
verifier = kwargs.get("data", {}).pop("oauth_verifier", None)
|
||||
oauth = OAuth1(
|
||||
resource_owner_key=token,
|
||||
resource_owner_secret=secret,
|
||||
client_key=self.source.consumer_key,
|
||||
client_secret=self.source.consumer_secret,
|
||||
reinlet_owner_key=token,
|
||||
reinlet_owner_secret=secret,
|
||||
client_key=self.inlet.consumer_key,
|
||||
client_secret=self.inlet.consumer_secret,
|
||||
verifier=verifier,
|
||||
callback_uri=callback,
|
||||
)
|
||||
|
@ -163,7 +163,7 @@ class OAuthClient(BaseOAuthClient):
|
|||
|
||||
@property
|
||||
def session_key(self):
|
||||
return "oauth-client-{0}-request-token".format(self.source.name)
|
||||
return "oauth-client-{0}-request-token".format(self.inlet.name)
|
||||
|
||||
|
||||
class OAuth2Client(BaseOAuthClient):
|
||||
|
@ -183,7 +183,7 @@ class OAuth2Client(BaseOAuthClient):
|
|||
if returned is not None:
|
||||
check = constant_time_compare(stored, returned)
|
||||
else:
|
||||
LOGGER.warning("No state parameter returned by the source.")
|
||||
LOGGER.warning("No state parameter returned by the inlet.")
|
||||
else:
|
||||
LOGGER.warning("No state stored in the sesssion.")
|
||||
return check
|
||||
|
@ -196,19 +196,19 @@ class OAuth2Client(BaseOAuthClient):
|
|||
return None
|
||||
if "code" in request.GET:
|
||||
args = {
|
||||
"client_id": self.source.consumer_key,
|
||||
"client_id": self.inlet.consumer_key,
|
||||
"redirect_uri": callback,
|
||||
"client_secret": self.source.consumer_secret,
|
||||
"client_secret": self.inlet.consumer_secret,
|
||||
"code": request.GET["code"],
|
||||
"grant_type": "authorization_code",
|
||||
}
|
||||
else:
|
||||
LOGGER.warning("No code returned by the source")
|
||||
LOGGER.warning("No code returned by the inlet")
|
||||
return None
|
||||
try:
|
||||
response = self.session.request(
|
||||
"post",
|
||||
self.source.access_token_url,
|
||||
self.inlet.access_token_url,
|
||||
data=args,
|
||||
headers=self._default_headers,
|
||||
**request_kwargs,
|
||||
|
@ -229,7 +229,7 @@ class OAuth2Client(BaseOAuthClient):
|
|||
"Get request parameters for redirect url."
|
||||
callback = request.build_absolute_uri(callback)
|
||||
args = {
|
||||
"client_id": self.source.consumer_key,
|
||||
"client_id": self.inlet.consumer_key,
|
||||
"redirect_uri": callback,
|
||||
"response_type": "code",
|
||||
}
|
||||
|
@ -264,12 +264,12 @@ class OAuth2Client(BaseOAuthClient):
|
|||
|
||||
@property
|
||||
def session_key(self):
|
||||
return "oauth-client-{0}-request-state".format(self.source.name)
|
||||
return "oauth-client-{0}-request-state".format(self.inlet.name)
|
||||
|
||||
|
||||
def get_client(source, token=""): # nosec
|
||||
"Return the API client for the given source."
|
||||
def get_client(inlet, token=""): # nosec
|
||||
"Return the API client for the given inlet."
|
||||
cls = OAuth2Client
|
||||
if source.request_token_url:
|
||||
if inlet.request_token_url:
|
||||
cls = OAuthClient
|
||||
return cls(source, token)
|
||||
return cls(inlet, token)
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
from django import forms
|
||||
|
||||
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
|
||||
from passbook.sources.oauth.models import OAuthSource
|
||||
from passbook.sources.oauth.types.manager import MANAGER
|
||||
from passbook.admin.forms.inlet import INLET_FORM_FIELDS
|
||||
from passbook.channels.in_oauth.models import OAuthInlet
|
||||
from passbook.channels.in_oauth.types.manager import MANAGER
|
||||
|
||||
|
||||
class OAuthSourceForm(forms.ModelForm):
|
||||
"""OAuthSource Form"""
|
||||
class OAuthInletForm(forms.ModelForm):
|
||||
"""OAuthInlet Form"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -19,8 +19,8 @@ class OAuthSourceForm(forms.ModelForm):
|
|||
|
||||
class Meta:
|
||||
|
||||
model = OAuthSource
|
||||
fields = SOURCE_FORM_FIELDS + [
|
||||
model = OAuthInlet
|
||||
fields = INLET_FORM_FIELDS + [
|
||||
"provider_type",
|
||||
"request_token_url",
|
||||
"authorization_url",
|
||||
|
@ -37,10 +37,10 @@ class OAuthSourceForm(forms.ModelForm):
|
|||
}
|
||||
|
||||
|
||||
class GitHubOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for GitHub"""
|
||||
class GitHubOAuthInletForm(OAuthInletForm):
|
||||
"""OAuth Inlet form with pre-determined URL for GitHub"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
class Meta(OAuthInletForm.Meta):
|
||||
|
||||
overrides = {
|
||||
"provider_type": "github",
|
||||
|
@ -51,10 +51,10 @@ class GitHubOAuthSourceForm(OAuthSourceForm):
|
|||
}
|
||||
|
||||
|
||||
class TwitterOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for Twitter"""
|
||||
class TwitterOAuthInletForm(OAuthInletForm):
|
||||
"""OAuth Inlet form with pre-determined URL for Twitter"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
class Meta(OAuthInletForm.Meta):
|
||||
|
||||
overrides = {
|
||||
"provider_type": "twitter",
|
||||
|
@ -68,10 +68,10 @@ class TwitterOAuthSourceForm(OAuthSourceForm):
|
|||
}
|
||||
|
||||
|
||||
class FacebookOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for Facebook"""
|
||||
class FacebookOAuthInletForm(OAuthInletForm):
|
||||
"""OAuth Inlet form with pre-determined URL for Facebook"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
class Meta(OAuthInletForm.Meta):
|
||||
|
||||
overrides = {
|
||||
"provider_type": "facebook",
|
||||
|
@ -82,10 +82,10 @@ class FacebookOAuthSourceForm(OAuthSourceForm):
|
|||
}
|
||||
|
||||
|
||||
class DiscordOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for Discord"""
|
||||
class DiscordOAuthInletForm(OAuthInletForm):
|
||||
"""OAuth Inlet form with pre-determined URL for Discord"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
class Meta(OAuthInletForm.Meta):
|
||||
|
||||
overrides = {
|
||||
"provider_type": "discord",
|
||||
|
@ -96,10 +96,10 @@ class DiscordOAuthSourceForm(OAuthSourceForm):
|
|||
}
|
||||
|
||||
|
||||
class GoogleOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for Google"""
|
||||
class GoogleOAuthInletForm(OAuthInletForm):
|
||||
"""OAuth Inlet form with pre-determined URL for Google"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
class Meta(OAuthInletForm.Meta):
|
||||
|
||||
overrides = {
|
||||
"provider_type": "google",
|
||||
|
@ -110,10 +110,10 @@ class GoogleOAuthSourceForm(OAuthSourceForm):
|
|||
}
|
||||
|
||||
|
||||
class AzureADOAuthSourceForm(OAuthSourceForm):
|
||||
"""OAuth Source form with pre-determined URL for AzureAD"""
|
||||
class AzureADOAuthInletForm(OAuthInletForm):
|
||||
"""OAuth Inlet form with pre-determined URL for AzureAD"""
|
||||
|
||||
class Meta(OAuthSourceForm.Meta):
|
||||
class Meta(OAuthInletForm.Meta):
|
||||
|
||||
overrides = {
|
||||
"provider_type": "azure-ad",
|
|
@ -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">
|
||||
{% if connections.exists %}
|
||||
<p>{% trans 'Connected.' %}</p>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_sources_oauth:oauth-client-disconnect' source_slug=source.slug %}">
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_channels_in_oauth:oauth-client-disconnect' source_slug=source.slug %}">
|
||||
{% trans 'Disconnect' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<p>Not connected.</p>
|
||||
<a class="pf-c-button pf-m-primary" href="{% url 'passbook_sources_oauth:oauth-client-login' source_slug=source.slug %}">
|
||||
<a class="pf-c-button pf-m-primary" href="{% url 'passbook_channels_in_oauth:oauth-client-login' source_slug=source.slug %}">
|
||||
{% trans 'Connect' %}
|
||||
</a>
|
||||
{% endif %}
|
|
@ -1,19 +1,19 @@
|
|||
"""AzureAD OAuth2 Views"""
|
||||
import uuid
|
||||
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback
|
||||
from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.channels.in_oauth.utils import user_get_or_create
|
||||
from passbook.channels.in_oauth.views.core import OAuthCallback
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name="Azure AD")
|
||||
@MANAGER.inlet(kind=RequestKind.callback, name="Azure AD")
|
||||
class AzureADOAuthCallback(OAuthCallback):
|
||||
"""AzureAD OAuth2 Callback"""
|
||||
|
||||
def get_user_id(self, source, info):
|
||||
def get_user_id(self, inlet, info):
|
||||
return uuid.UUID(info.get("objectId")).int
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
def get_or_create_user(self, inlet, access, info):
|
||||
user_data = {
|
||||
"username": info.get("displayName"),
|
||||
"email": info.get("mail", None) or info.get("otherMails")[0],
|
|
@ -1,24 +1,24 @@
|
|||
"""Discord OAuth Views"""
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.channels.in_oauth.utils import user_get_or_create
|
||||
from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name="Discord")
|
||||
@MANAGER.inlet(kind=RequestKind.redirect, name="Discord")
|
||||
class DiscordOAuthRedirect(OAuthRedirect):
|
||||
"""Discord OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
def get_additional_parameters(self, inlet):
|
||||
return {
|
||||
"scope": "email identify",
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name="Discord")
|
||||
@MANAGER.inlet(kind=RequestKind.callback, name="Discord")
|
||||
class DiscordOAuth2Callback(OAuthCallback):
|
||||
"""Discord OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
def get_or_create_user(self, inlet, access, info):
|
||||
user_data = {
|
||||
"username": info.get("username"),
|
||||
"email": info.get("email", "None"),
|
|
@ -1,24 +1,24 @@
|
|||
"""Facebook OAuth Views"""
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.channels.in_oauth.utils import user_get_or_create
|
||||
from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name="Facebook")
|
||||
@MANAGER.inlet(kind=RequestKind.redirect, name="Facebook")
|
||||
class FacebookOAuthRedirect(OAuthRedirect):
|
||||
"""Facebook OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
def get_additional_parameters(self, inlet):
|
||||
return {
|
||||
"scope": "email",
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name="Facebook")
|
||||
@MANAGER.inlet(kind=RequestKind.callback, name="Facebook")
|
||||
class FacebookOAuth2Callback(OAuthCallback):
|
||||
"""Facebook OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
def get_or_create_user(self, inlet, access, info):
|
||||
user_data = {
|
||||
"username": info.get("name"),
|
||||
"email": info.get("email", ""),
|
|
@ -1,14 +1,14 @@
|
|||
"""GitHub OAuth Views"""
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback
|
||||
from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.channels.in_oauth.utils import user_get_or_create
|
||||
from passbook.channels.in_oauth.views.core import OAuthCallback
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name="GitHub")
|
||||
@MANAGER.inlet(kind=RequestKind.callback, name="GitHub")
|
||||
class GitHubOAuth2Callback(OAuthCallback):
|
||||
"""GitHub OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
def get_or_create_user(self, inlet, access, info):
|
||||
user_data = {
|
||||
"username": info.get("login"),
|
||||
"email": info.get("email", ""),
|
|
@ -1,24 +1,24 @@
|
|||
"""Google OAuth Views"""
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.channels.in_oauth.utils import user_get_or_create
|
||||
from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name="Google")
|
||||
@MANAGER.inlet(kind=RequestKind.redirect, name="Google")
|
||||
class GoogleOAuthRedirect(OAuthRedirect):
|
||||
"""Google OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
def get_additional_parameters(self, inlet):
|
||||
return {
|
||||
"scope": "email profile",
|
||||
}
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name="Google")
|
||||
@MANAGER.inlet(kind=RequestKind.callback, name="Google")
|
||||
class GoogleOAuth2Callback(OAuthCallback):
|
||||
"""Google OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
def get_or_create_user(self, inlet, access, info):
|
||||
user_data = {
|
||||
"username": info.get("email"),
|
||||
"email": info.get("email", ""),
|
|
@ -1,10 +1,10 @@
|
|||
"""Source type manager"""
|
||||
"""Inlet type manager"""
|
||||
from enum import Enum
|
||||
|
||||
from django.utils.text import slugify
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -16,21 +16,21 @@ class RequestKind(Enum):
|
|||
redirect = "redirect"
|
||||
|
||||
|
||||
class SourceTypeManager:
|
||||
"""Manager to hold all Source types."""
|
||||
class InletTypeManager:
|
||||
"""Manager to hold all Inlet types."""
|
||||
|
||||
__source_types = {}
|
||||
__inlet_types = {}
|
||||
__names = []
|
||||
|
||||
def source(self, kind, name):
|
||||
def inlet(self, kind, name):
|
||||
"""Class decorator to register classes inline."""
|
||||
|
||||
def inner_wrapper(cls):
|
||||
if kind not in self.__source_types:
|
||||
self.__source_types[kind] = {}
|
||||
self.__source_types[kind][name.lower()] = cls
|
||||
if kind not in self.__inlet_types:
|
||||
self.__inlet_types[kind] = {}
|
||||
self.__inlet_types[kind][name.lower()] = cls
|
||||
self.__names.append(name)
|
||||
LOGGER.debug("Registered source", source_class=cls.__name__, kind=kind)
|
||||
LOGGER.debug("Registered inlet", inlet_class=cls.__name__, kind=kind)
|
||||
return cls
|
||||
|
||||
return inner_wrapper
|
||||
|
@ -39,11 +39,11 @@ class SourceTypeManager:
|
|||
"""Get list of tuples of all registered names"""
|
||||
return [(slugify(x), x) for x in set(self.__names)]
|
||||
|
||||
def find(self, source, kind):
|
||||
"""Find fitting Source Type"""
|
||||
if kind in self.__source_types:
|
||||
if source.provider_type in self.__source_types[kind]:
|
||||
return self.__source_types[kind][source.provider_type]
|
||||
def find(self, inlet, kind):
|
||||
"""Find fitting Inlet Type"""
|
||||
if kind in self.__inlet_types:
|
||||
if inlet.provider_type in self.__inlet_types[kind]:
|
||||
return self.__inlet_types[kind][inlet.provider_type]
|
||||
# Return defaults
|
||||
if kind == RequestKind.callback:
|
||||
return OAuthCallback
|
||||
|
@ -52,4 +52,4 @@ class SourceTypeManager:
|
|||
raise KeyError
|
||||
|
||||
|
||||
MANAGER = SourceTypeManager()
|
||||
MANAGER = InletTypeManager()
|
|
@ -1,17 +1,17 @@
|
|||
"""Reddit OAuth Views"""
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from passbook.sources.oauth.clients import OAuth2Client
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
from passbook.channels.in_oauth.clients import OAuth2Client
|
||||
from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.channels.in_oauth.utils import user_get_or_create
|
||||
from passbook.channels.in_oauth.views.core import OAuthCallback, OAuthRedirect
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.redirect, name="reddit")
|
||||
@MANAGER.inlet(kind=RequestKind.redirect, name="reddit")
|
||||
class RedditOAuthRedirect(OAuthRedirect):
|
||||
"""Reddit OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
def get_additional_parameters(self, inlet):
|
||||
return {
|
||||
"scope": "identity",
|
||||
"duration": "permanent",
|
||||
|
@ -23,19 +23,19 @@ class RedditOAuth2Client(OAuth2Client):
|
|||
|
||||
def get_access_token(self, request, callback=None, **request_kwargs):
|
||||
"Fetch access token from callback request."
|
||||
auth = HTTPBasicAuth(self.source.consumer_key, self.source.consumer_secret)
|
||||
auth = HTTPBasicAuth(self.inlet.consumer_key, self.inlet.consumer_secret)
|
||||
return super(RedditOAuth2Client, self).get_access_token(
|
||||
request, callback, auth=auth
|
||||
)
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name="reddit")
|
||||
@MANAGER.inlet(kind=RequestKind.callback, name="reddit")
|
||||
class RedditOAuth2Callback(OAuthCallback):
|
||||
"""Reddit OAuth2 Callback"""
|
||||
|
||||
client_class = RedditOAuth2Client
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
def get_or_create_user(self, inlet, access, info):
|
||||
user_data = {
|
||||
"username": info.get("name"),
|
||||
"email": None,
|
|
@ -1,14 +1,14 @@
|
|||
"""Twitter OAuth Views"""
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.sources.oauth.utils import user_get_or_create
|
||||
from passbook.sources.oauth.views.core import OAuthCallback
|
||||
from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.channels.in_oauth.utils import user_get_or_create
|
||||
from passbook.channels.in_oauth.views.core import OAuthCallback
|
||||
|
||||
|
||||
@MANAGER.source(kind=RequestKind.callback, name="Twitter")
|
||||
@MANAGER.inlet(kind=RequestKind.callback, name="Twitter")
|
||||
class TwitterOAuthCallback(OAuthCallback):
|
||||
"""Twitter OAuth2 Callback"""
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
def get_or_create_user(self, inlet, access, info):
|
||||
user_data = {
|
||||
"username": info.get("screen_name"),
|
||||
"email": info.get("email", ""),
|
|
@ -2,27 +2,27 @@
|
|||
|
||||
from django.urls import path
|
||||
|
||||
from passbook.sources.oauth.types.manager import RequestKind
|
||||
from passbook.sources.oauth.views import core, dispatcher, user
|
||||
from passbook.channels.in_oauth.types.manager import RequestKind
|
||||
from passbook.channels.in_oauth.views import core, dispatcher, user
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"login/<slug:source_slug>/",
|
||||
"login/<slug:inlet_slug>/",
|
||||
dispatcher.DispatcherView.as_view(kind=RequestKind.redirect),
|
||||
name="oauth-client-login",
|
||||
),
|
||||
path(
|
||||
"callback/<slug:source_slug>/",
|
||||
"callback/<slug:inlet_slug>/",
|
||||
dispatcher.DispatcherView.as_view(kind=RequestKind.callback),
|
||||
name="oauth-client-callback",
|
||||
),
|
||||
path(
|
||||
"disconnect/<slug:source_slug>/",
|
||||
"disconnect/<slug:inlet_slug>/",
|
||||
core.DisconnectView.as_view(),
|
||||
name="oauth-client-disconnect",
|
||||
),
|
||||
path(
|
||||
"user/<slug:source_slug>/",
|
||||
"user/<slug:inlet_slug>/",
|
||||
user.UserSettingsView.as_view(),
|
||||
name="oauth-client-user",
|
||||
),
|
|
@ -13,6 +13,8 @@ from django.views.generic import RedirectView, View
|
|||
from structlog import get_logger
|
||||
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.channels.in_oauth.clients import get_client
|
||||
from passbook.channels.in_oauth.models import OAuthInlet, UserOAuthInletConnection
|
||||
from passbook.flows.models import Flow, FlowDesignation
|
||||
from passbook.flows.planner import (
|
||||
PLAN_CONTEXT_PENDING_USER,
|
||||
|
@ -21,8 +23,6 @@ from passbook.flows.planner import (
|
|||
)
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
from passbook.lib.utils.urls import redirect_with_qs
|
||||
from passbook.sources.oauth.clients import get_client
|
||||
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
@ -30,49 +30,49 @@ LOGGER = get_logger()
|
|||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class OAuthClientMixin:
|
||||
"Mixin for getting OAuth client for a source."
|
||||
"Mixin for getting OAuth client for a inlet."
|
||||
|
||||
client_class: Optional[Callable] = None
|
||||
|
||||
def get_client(self, source):
|
||||
"Get instance of the OAuth client for this source."
|
||||
def get_client(self, inlet):
|
||||
"Get instance of the OAuth client for this inlet."
|
||||
if self.client_class is not None:
|
||||
# pylint: disable=not-callable
|
||||
return self.client_class(source)
|
||||
return get_client(source)
|
||||
return self.client_class(inlet)
|
||||
return get_client(inlet)
|
||||
|
||||
|
||||
class OAuthRedirect(OAuthClientMixin, RedirectView):
|
||||
"Redirect user to OAuth source to enable access."
|
||||
"Redirect user to OAuth inlet to enable access."
|
||||
|
||||
permanent = False
|
||||
params = None
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_additional_parameters(self, source):
|
||||
"Return additional redirect parameters for this source."
|
||||
def get_additional_parameters(self, inlet):
|
||||
"Return additional redirect parameters for this inlet."
|
||||
return self.params or {}
|
||||
|
||||
def get_callback_url(self, source):
|
||||
"Return the callback url for this source."
|
||||
def get_callback_url(self, inlet):
|
||||
"Return the callback url for this inlet."
|
||||
return reverse(
|
||||
"passbook_sources_oauth:oauth-client-callback",
|
||||
kwargs={"source_slug": source.slug},
|
||||
"passbook_channels_in_oauth:oauth-client-callback",
|
||||
kwargs={"inlet_slug": inlet.slug},
|
||||
)
|
||||
|
||||
def get_redirect_url(self, **kwargs):
|
||||
"Build redirect url for a given source."
|
||||
slug = kwargs.get("source_slug", "")
|
||||
"Build redirect url for a given inlet."
|
||||
slug = kwargs.get("inlet_slug", "")
|
||||
try:
|
||||
source = OAuthSource.objects.get(slug=slug)
|
||||
except OAuthSource.DoesNotExist:
|
||||
raise Http404("Unknown OAuth source '%s'." % slug)
|
||||
inlet = OAuthInlet.objects.get(slug=slug)
|
||||
except OAuthInlet.DoesNotExist:
|
||||
raise Http404("Unknown OAuth inlet '%s'." % slug)
|
||||
else:
|
||||
if not source.enabled:
|
||||
raise Http404("source %s is not enabled." % slug)
|
||||
client = self.get_client(source)
|
||||
callback = self.get_callback_url(source)
|
||||
params = self.get_additional_parameters(source)
|
||||
if not inlet.enabled:
|
||||
raise Http404("inlet %s is not enabled." % slug)
|
||||
client = self.get_client(inlet)
|
||||
callback = self.get_callback_url(inlet)
|
||||
params = self.get_additional_parameters(inlet)
|
||||
return client.get_redirect_url(
|
||||
self.request, callback=callback, parameters=params
|
||||
)
|
||||
|
@ -81,85 +81,85 @@ class OAuthRedirect(OAuthClientMixin, RedirectView):
|
|||
class OAuthCallback(OAuthClientMixin, View):
|
||||
"Base OAuth callback view."
|
||||
|
||||
source_id = None
|
||||
source = None
|
||||
inlet_id = None
|
||||
inlet = None
|
||||
|
||||
def get(self, request, *_, **kwargs):
|
||||
"""View Get handler"""
|
||||
slug = kwargs.get("source_slug", "")
|
||||
slug = kwargs.get("inlet_slug", "")
|
||||
try:
|
||||
self.source = OAuthSource.objects.get(slug=slug)
|
||||
except OAuthSource.DoesNotExist:
|
||||
raise Http404("Unknown OAuth source '%s'." % slug)
|
||||
self.inlet = OAuthInlet.objects.get(slug=slug)
|
||||
except OAuthInlet.DoesNotExist:
|
||||
raise Http404("Unknown OAuth inlet '%s'." % slug)
|
||||
else:
|
||||
if not self.source.enabled:
|
||||
raise Http404("source %s is not enabled." % slug)
|
||||
client = self.get_client(self.source)
|
||||
callback = self.get_callback_url(self.source)
|
||||
if not self.inlet.enabled:
|
||||
raise Http404("inlet %s is not enabled." % slug)
|
||||
client = self.get_client(self.inlet)
|
||||
callback = self.get_callback_url(self.inlet)
|
||||
# Fetch access token
|
||||
token = client.get_access_token(self.request, callback=callback)
|
||||
if token is None:
|
||||
return self.handle_login_failure(
|
||||
self.source, "Could not retrieve token."
|
||||
self.inlet, "Could not retrieve token."
|
||||
)
|
||||
if "error" in token:
|
||||
return self.handle_login_failure(self.source, token["error"])
|
||||
return self.handle_login_failure(self.inlet, token["error"])
|
||||
# Fetch profile info
|
||||
info = client.get_profile_info(token)
|
||||
if info is None:
|
||||
return self.handle_login_failure(
|
||||
self.source, "Could not retrieve profile."
|
||||
self.inlet, "Could not retrieve profile."
|
||||
)
|
||||
identifier = self.get_user_id(self.source, info)
|
||||
identifier = self.get_user_id(self.inlet, info)
|
||||
if identifier is None:
|
||||
return self.handle_login_failure(self.source, "Could not determine id.")
|
||||
return self.handle_login_failure(self.inlet, "Could not determine id.")
|
||||
# Get or create access record
|
||||
defaults = {
|
||||
"access_token": token.get("access_token"),
|
||||
}
|
||||
existing = UserOAuthSourceConnection.objects.filter(
|
||||
source=self.source, identifier=identifier
|
||||
existing = UserOAuthInletConnection.objects.filter(
|
||||
inlet=self.inlet, identifier=identifier
|
||||
)
|
||||
|
||||
if existing.exists():
|
||||
connection = existing.first()
|
||||
connection.access_token = token.get("access_token")
|
||||
UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(
|
||||
UserOAuthInletConnection.objects.filter(pk=connection.pk).update(
|
||||
**defaults
|
||||
)
|
||||
else:
|
||||
connection = UserOAuthSourceConnection(
|
||||
source=self.source,
|
||||
connection = UserOAuthInletConnection(
|
||||
inlet=self.inlet,
|
||||
identifier=identifier,
|
||||
access_token=token.get("access_token"),
|
||||
)
|
||||
user = authenticate(
|
||||
source=self.source, identifier=identifier, request=request
|
||||
inlet=self.inlet, identifier=identifier, request=request
|
||||
)
|
||||
if user is None:
|
||||
LOGGER.debug("Handling new user", source=self.source)
|
||||
return self.handle_new_user(self.source, connection, info)
|
||||
LOGGER.debug("Handling existing user", source=self.source)
|
||||
return self.handle_existing_user(self.source, user, connection, info)
|
||||
LOGGER.debug("Handling new user", inlet=self.inlet)
|
||||
return self.handle_new_user(self.inlet, connection, info)
|
||||
LOGGER.debug("Handling existing user", inlet=self.inlet)
|
||||
return self.handle_existing_user(self.inlet, user, connection, info)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_callback_url(self, source):
|
||||
def get_callback_url(self, inlet):
|
||||
"Return callback url if different than the current url."
|
||||
return False
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_error_redirect(self, source, reason):
|
||||
def get_error_redirect(self, inlet, reason):
|
||||
"Return url to redirect on login failure."
|
||||
return settings.LOGIN_URL
|
||||
|
||||
def get_or_create_user(self, source, access, info):
|
||||
def get_or_create_user(self, inlet, access, info):
|
||||
"Create a shell auth.User."
|
||||
raise NotImplementedError()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_user_id(self, source, info):
|
||||
def get_user_id(self, inlet, info):
|
||||
"Return unique identifier from the profile info."
|
||||
id_key = self.source_id or "id"
|
||||
id_key = self.inlet_id or "id"
|
||||
result = info
|
||||
try:
|
||||
for key in id_key.split("."):
|
||||
|
@ -168,10 +168,10 @@ class OAuthCallback(OAuthClientMixin, View):
|
|||
except KeyError:
|
||||
return None
|
||||
|
||||
def handle_login(self, user, source, access):
|
||||
def handle_login(self, user, inlet, access):
|
||||
"""Prepare Authentication Plan, redirect user FlowExecutor"""
|
||||
user = authenticate(
|
||||
source=access.source, identifier=access.identifier, request=self.request
|
||||
inlet=access.inlet, identifier=access.identifier, request=self.request
|
||||
)
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
flow = get_object_or_404(Flow, designation=FlowDesignation.AUTHENTICATION)
|
||||
|
@ -186,24 +186,24 @@ class OAuthCallback(OAuthClientMixin, View):
|
|||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle_existing_user(self, source, user, access, info):
|
||||
def handle_existing_user(self, inlet, user, access, info):
|
||||
"Login user and redirect."
|
||||
messages.success(
|
||||
self.request,
|
||||
_(
|
||||
"Successfully authenticated with %(source)s!"
|
||||
% {"source": self.source.name}
|
||||
"Successfully authenticated with %(inlet)s!"
|
||||
% {"inlet": self.inlet.name}
|
||||
),
|
||||
)
|
||||
return self.handle_login(user, source, access)
|
||||
return self.handle_login(user, inlet, access)
|
||||
|
||||
def handle_login_failure(self, source, reason):
|
||||
def handle_login_failure(self, inlet, reason):
|
||||
"Message user and redirect on error."
|
||||
LOGGER.warning("Authentication Failure", reason=reason)
|
||||
messages.error(self.request, _("Authentication Failed."))
|
||||
return redirect(self.get_error_redirect(source, reason))
|
||||
return redirect(self.get_error_redirect(inlet, reason))
|
||||
|
||||
def handle_new_user(self, source, access, info):
|
||||
def handle_new_user(self, inlet, access, info):
|
||||
"Create a shell auth.User and redirect."
|
||||
was_authenticated = False
|
||||
if self.request.user.is_authenticated:
|
||||
|
@ -211,52 +211,52 @@ class OAuthCallback(OAuthClientMixin, View):
|
|||
user = self.request.user
|
||||
was_authenticated = True
|
||||
else:
|
||||
user = self.get_or_create_user(source, access, info)
|
||||
user = self.get_or_create_user(inlet, access, info)
|
||||
access.user = user
|
||||
access.save()
|
||||
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
|
||||
UserOAuthInletConnection.objects.filter(pk=access.pk).update(user=user)
|
||||
Event.new(
|
||||
EventAction.CUSTOM, message="Linked OAuth Source", source=source
|
||||
EventAction.CUSTOM, message="Linked OAuth Inlet", inlet=inlet
|
||||
).from_http(self.request)
|
||||
if was_authenticated:
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully linked %(source)s!" % {"source": self.source.name}),
|
||||
_("Successfully linked %(inlet)s!" % {"inlet": self.inlet.name}),
|
||||
)
|
||||
return redirect(
|
||||
reverse(
|
||||
"passbook_sources_oauth:oauth-client-user",
|
||||
kwargs={"source_slug": self.source.slug},
|
||||
"passbook_channels_in_oauth:oauth-client-user",
|
||||
kwargs={"inlet_slug": self.inlet.slug},
|
||||
)
|
||||
)
|
||||
# User was not authenticated, new user has been created
|
||||
user = authenticate(
|
||||
source=access.source, identifier=access.identifier, request=self.request
|
||||
inlet=access.inlet, identifier=access.identifier, request=self.request
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
_(
|
||||
"Successfully authenticated with %(source)s!"
|
||||
% {"source": self.source.name}
|
||||
"Successfully authenticated with %(inlet)s!"
|
||||
% {"inlet": self.inlet.name}
|
||||
),
|
||||
)
|
||||
return self.handle_login(user, source, access)
|
||||
return self.handle_login(user, inlet, access)
|
||||
|
||||
|
||||
class DisconnectView(LoginRequiredMixin, View):
|
||||
"""Delete connection with source"""
|
||||
"""Delete connection with inlet"""
|
||||
|
||||
source = None
|
||||
inlet = None
|
||||
aas = None
|
||||
|
||||
def dispatch(self, request, source_slug):
|
||||
self.source = get_object_or_404(OAuthSource, slug=source_slug)
|
||||
def dispatch(self, request, inlet_slug):
|
||||
self.inlet = get_object_or_404(OAuthInlet, slug=inlet_slug)
|
||||
self.aas = get_object_or_404(
|
||||
UserOAuthSourceConnection, source=self.source, user=request.user
|
||||
UserOAuthInletConnection, inlet=self.inlet, user=request.user
|
||||
)
|
||||
return super().dispatch(request, source_slug)
|
||||
return super().dispatch(request, inlet_slug)
|
||||
|
||||
def post(self, request, source_slug):
|
||||
def post(self, request, inlet_slug):
|
||||
"""Delete connection object"""
|
||||
if "confirmdelete" in request.POST:
|
||||
# User confirmed deletion
|
||||
|
@ -264,23 +264,23 @@ class DisconnectView(LoginRequiredMixin, View):
|
|||
messages.success(request, _("Connection successfully deleted"))
|
||||
return redirect(
|
||||
reverse(
|
||||
"passbook_sources_oauth:oauth-client-user",
|
||||
kwargs={"source_slug": self.source.slug},
|
||||
"passbook_channels_in_oauth:oauth-client-user",
|
||||
kwargs={"inlet_slug": self.inlet.slug},
|
||||
)
|
||||
)
|
||||
return self.get(request, source_slug)
|
||||
return self.get(request, inlet_slug)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request, source_slug):
|
||||
def get(self, request, inlet_slug):
|
||||
"""Show delete form"""
|
||||
return render(
|
||||
request,
|
||||
"generic/delete.html",
|
||||
{
|
||||
"object": self.source,
|
||||
"object": self.inlet,
|
||||
"delete_url": reverse(
|
||||
"passbook_sources_oauth:oauth-client-disconnect",
|
||||
kwargs={"source_slug": self.source.slug,},
|
||||
"passbook_channels_in_oauth:oauth-client-disconnect",
|
||||
kwargs={"inlet_slug": self.inlet.slug,},
|
||||
),
|
||||
},
|
||||
)
|
|
@ -3,8 +3,8 @@ from django.http import Http404
|
|||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
|
||||
from passbook.sources.oauth.models import OAuthSource
|
||||
from passbook.sources.oauth.types.manager import MANAGER, RequestKind
|
||||
from passbook.channels.in_oauth.models import OAuthInlet
|
||||
from passbook.channels.in_oauth.types.manager import MANAGER, RequestKind
|
||||
|
||||
|
||||
class DispatcherView(View):
|
||||
|
@ -13,10 +13,10 @@ class DispatcherView(View):
|
|||
kind = ""
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""Find Source by slug and forward request"""
|
||||
slug = kwargs.get("source_slug", None)
|
||||
"""Find Inlet by slug and forward request"""
|
||||
slug = kwargs.get("inlet_slug", None)
|
||||
if not slug:
|
||||
raise Http404
|
||||
source = get_object_or_404(OAuthSource, slug=slug)
|
||||
view = MANAGER.find(source, kind=RequestKind(self.kind))
|
||||
inlet = get_object_or_404(OAuthInlet, slug=slug)
|
||||
view = MANAGER.find(inlet, kind=RequestKind(self.kind))
|
||||
return view.as_view()(*args, **kwargs)
|
|
@ -3,7 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
|||
from django.shortcuts import get_object_or_404
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from passbook.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from passbook.channels.in_oauth.models import OAuthInlet, UserOAuthInletConnection
|
||||
|
||||
|
||||
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||
|
@ -12,10 +12,10 @@ class UserSettingsView(LoginRequiredMixin, TemplateView):
|
|||
template_name = "oauth_client/user.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
source = get_object_or_404(OAuthSource, slug=self.kwargs.get("source_slug"))
|
||||
connections = UserOAuthSourceConnection.objects.filter(
|
||||
user=self.request.user, source=source
|
||||
inlet = get_object_or_404(OAuthInlet, slug=self.kwargs.get("inlet_slug"))
|
||||
connections = UserOAuthInletConnection.objects.filter(
|
||||
user=self.request.user, inlet=inlet
|
||||
)
|
||||
kwargs["source"] = source
|
||||
kwargs["inlet"] = inlet
|
||||
kwargs["connections"] = connections
|
||||
return super().get_context_data(**kwargs)
|
|
@ -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.utils.translation import gettext as _
|
||||
|
||||
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
|
||||
from passbook.sources.saml.models import SAMLSource
|
||||
from passbook.admin.forms.inlet import INLET_FORM_FIELDS
|
||||
from passbook.channels.in_saml.models import SAMLInlet
|
||||
|
||||
|
||||
class SAMLSourceForm(forms.ModelForm):
|
||||
"""SAML Provider form"""
|
||||
class SAMLInletForm(forms.ModelForm):
|
||||
"""SAML Inlet form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = SAMLSource
|
||||
fields = SOURCE_FORM_FIELDS + [
|
||||
model = SAMLInlet
|
||||
fields = INLET_FORM_FIELDS + [
|
||||
"issuer",
|
||||
"idp_url",
|
||||
"idp_logout_url",
|
|
@ -0,0 +1,68 @@
|
|||
# Generated by Django 3.0.5 on 2020-05-15 19:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("passbook_crypto", "0001_initial"),
|
||||
("passbook_core", "__first__"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SAMLInlet",
|
||||
fields=[
|
||||
(
|
||||
"inlet_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_core.Inlet",
|
||||
),
|
||||
),
|
||||
(
|
||||
"issuer",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Also known as Entity ID. Defaults the Metadata URL.",
|
||||
verbose_name="Issuer",
|
||||
),
|
||||
),
|
||||
("idp_url", models.URLField(verbose_name="IDP URL")),
|
||||
(
|
||||
"idp_logout_url",
|
||||
models.URLField(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
verbose_name="IDP Logout URL",
|
||||
),
|
||||
),
|
||||
("auto_logout", models.BooleanField(default=False)),
|
||||
(
|
||||
"signing_kp",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
help_text="Certificate Key Pair of the IdP which Assertions are validated against.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="passbook_crypto.CertificateKeyPair",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "SAML Inlet",
|
||||
"verbose_name_plural": "SAML Inlets",
|
||||
},
|
||||
bases=("passbook_core.inlet",),
|
||||
),
|
||||
]
|
|
@ -3,13 +3,13 @@ from django.db import models
|
|||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.core.models import Source
|
||||
from passbook.core.models import Inlet
|
||||
from passbook.core.types import UILoginButton
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
|
||||
|
||||
class SAMLSource(Source):
|
||||
"""SAML Source"""
|
||||
class SAMLInlet(Inlet):
|
||||
"""SAML Inlet"""
|
||||
|
||||
issuer = models.TextField(
|
||||
blank=True,
|
||||
|
@ -34,14 +34,14 @@ class SAMLSource(Source):
|
|||
on_delete=models.SET_NULL,
|
||||
)
|
||||
|
||||
form = "passbook.sources.saml.forms.SAMLSourceForm"
|
||||
form = "passbook.channels.in_saml.forms.SAMLInletForm"
|
||||
|
||||
@property
|
||||
def ui_login_button(self) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
name=self.name,
|
||||
url=reverse_lazy(
|
||||
"passbook_sources_saml:login", kwargs={"source_slug": self.slug}
|
||||
"passbook_channels_in_saml:login", kwargs={"inlet_slug": self.slug}
|
||||
),
|
||||
icon_path="",
|
||||
)
|
||||
|
@ -49,14 +49,14 @@ class SAMLSource(Source):
|
|||
@property
|
||||
def ui_additional_info(self) -> str:
|
||||
metadata_url = reverse_lazy(
|
||||
"passbook_sources_saml:metadata", kwargs={"source_slug": self.slug}
|
||||
"passbook_channels_in_saml:metadata", kwargs={"inlet_slug": self.slug}
|
||||
)
|
||||
return f'<a href="{metadata_url}" class="btn btn-default btn-sm">Metadata Download</a>'
|
||||
|
||||
def __str__(self):
|
||||
return f"SAML Source {self.name}"
|
||||
return f"SAML Inlet {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("SAML Source")
|
||||
verbose_name_plural = _("SAML Sources")
|
||||
verbose_name = _("SAML Inlet")
|
||||
verbose_name_plural = _("SAML Inlets")
|
|
@ -1,4 +1,4 @@
|
|||
"""passbook saml source processor"""
|
||||
"""passbook saml inlet processor"""
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from defusedxml import ElementTree
|
||||
|
@ -6,13 +6,13 @@ from django.http import HttpRequest
|
|||
from signxml import XMLVerifier
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.providers.saml.utils.encoding import decode_base64_and_inflate
|
||||
from passbook.sources.saml.exceptions import (
|
||||
from passbook.channels.in_saml.exceptions import (
|
||||
MissingSAMLResponse,
|
||||
UnsupportedNameIDFormat,
|
||||
)
|
||||
from passbook.sources.saml.models import SAMLSource
|
||||
from passbook.channels.in_saml.models import SAMLInlet
|
||||
from passbook.channels.out_saml.utils.encoding import decode_base64_and_inflate
|
||||
from passbook.core.models import User
|
||||
|
||||
LOGGER = get_logger()
|
||||
if TYPE_CHECKING:
|
||||
|
@ -22,13 +22,13 @@ if TYPE_CHECKING:
|
|||
class Processor:
|
||||
"""SAML Response Processor"""
|
||||
|
||||
_source: SAMLSource
|
||||
_inlet: SAMLInlet
|
||||
|
||||
_root: "Element"
|
||||
_root_xml: str
|
||||
|
||||
def __init__(self, source: SAMLSource):
|
||||
self._source = source
|
||||
def __init__(self, inlet: SAMLInlet):
|
||||
self._inlet = inlet
|
||||
|
||||
def parse(self, request: HttpRequest):
|
||||
"""Check if `request` contains SAML Response data, parse and validate it."""
|
||||
|
@ -46,7 +46,7 @@ class Processor:
|
|||
def _verify_signed(self):
|
||||
"""Verify SAML Response's Signature"""
|
||||
verifier = XMLVerifier()
|
||||
verifier.verify(self._root_xml, x509_cert=self._source.signing_kp.certificate)
|
||||
verifier.verify(self._root_xml, x509_cert=self._inlet.signing_kp.certificate)
|
||||
|
||||
def _get_email(self) -> Optional[str]:
|
||||
"""
|
|
@ -1,7 +1,7 @@
|
|||
"""saml sp urls"""
|
||||
from django.urls import path
|
||||
|
||||
from passbook.sources.saml.views import ACSView, InitiateView, MetadataView, SLOView
|
||||
from passbook.channels.in_saml.views import ACSView, InitiateView, MetadataView, SLOView
|
||||
|
||||
urlpatterns = [
|
||||
path("<slug:source_slug>/", InitiateView.as_view(), name="login"),
|
|
@ -0,0 +1,20 @@
|
|||
"""saml sp helpers"""
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from passbook.channels.in_saml.models import SAMLInlet
|
||||
|
||||
|
||||
def get_issuer(request: HttpRequest, inlet: SAMLInlet) -> str:
|
||||
"""Get Inlet's Issuer, falling back to our Metadata URL if none is set"""
|
||||
issuer = inlet.issuer
|
||||
if issuer is None:
|
||||
return build_full_url("metadata", request, inlet)
|
||||
return issuer
|
||||
|
||||
|
||||
def build_full_url(view: str, request: HttpRequest, inlet: SAMLInlet) -> str:
|
||||
"""Build Full ACS URL to be used in IDP"""
|
||||
return request.build_absolute_uri(
|
||||
reverse(f"passbook_channels_in_saml:{view}", kwargs={"inlet_slug": inlet.slug})
|
||||
)
|
|
@ -7,36 +7,36 @@ from django.views import View
|
|||
from django.views.decorators.csrf import csrf_exempt
|
||||
from signxml.util import strip_pem_header
|
||||
|
||||
from passbook.lib.views import bad_request_message
|
||||
from passbook.providers.saml.utils import get_random_id, render_xml
|
||||
from passbook.providers.saml.utils.encoding import nice64
|
||||
from passbook.providers.saml.utils.time import get_time_string
|
||||
from passbook.sources.saml.exceptions import (
|
||||
from passbook.channels.in_saml.exceptions import (
|
||||
MissingSAMLResponse,
|
||||
UnsupportedNameIDFormat,
|
||||
)
|
||||
from passbook.sources.saml.models import SAMLSource
|
||||
from passbook.sources.saml.processors.base import Processor
|
||||
from passbook.sources.saml.utils import build_full_url, get_issuer
|
||||
from passbook.sources.saml.xml_render import get_authnrequest_xml
|
||||
from passbook.channels.in_saml.models import SAMLInlet
|
||||
from passbook.channels.in_saml.processors.base import Processor
|
||||
from passbook.channels.in_saml.utils import build_full_url, get_issuer
|
||||
from passbook.channels.in_saml.xml_render import get_authnrequest_xml
|
||||
from passbook.channels.out_saml.utils import get_random_id, render_xml
|
||||
from passbook.channels.out_saml.utils.encoding import nice64
|
||||
from passbook.channels.out_saml.utils.time import get_time_string
|
||||
from passbook.lib.views import bad_request_message
|
||||
|
||||
|
||||
class InitiateView(View):
|
||||
"""Get the Form with SAML Request, which sends us to the IDP"""
|
||||
|
||||
def get(self, request: HttpRequest, source_slug: str) -> HttpResponse:
|
||||
def get(self, request: HttpRequest, inlet_slug: str) -> HttpResponse:
|
||||
"""Replies with an XHTML SSO Request."""
|
||||
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug)
|
||||
if not source.enabled:
|
||||
inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug)
|
||||
if not inlet.enabled:
|
||||
raise Http404
|
||||
sso_destination = request.GET.get("next", None)
|
||||
request.session["sso_destination"] = sso_destination
|
||||
parameters = {
|
||||
"ACS_URL": build_full_url("acs", request, source),
|
||||
"DESTINATION": source.idp_url,
|
||||
"ACS_URL": build_full_url("acs", request, inlet),
|
||||
"DESTINATION": inlet.idp_url,
|
||||
"AUTHN_REQUEST_ID": get_random_id(),
|
||||
"ISSUE_INSTANT": get_time_string(),
|
||||
"ISSUER": get_issuer(request, source),
|
||||
"ISSUER": get_issuer(request, inlet),
|
||||
}
|
||||
authn_req = get_authnrequest_xml(parameters, signed=False)
|
||||
_request = nice64(str.encode(authn_req))
|
||||
|
@ -44,10 +44,10 @@ class InitiateView(View):
|
|||
request,
|
||||
"saml/sp/login.html",
|
||||
{
|
||||
"request_url": source.idp_url,
|
||||
"request_url": inlet.idp_url,
|
||||
"request": _request,
|
||||
"token": sso_destination,
|
||||
"source": source,
|
||||
"inlet": inlet,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -56,12 +56,12 @@ class InitiateView(View):
|
|||
class ACSView(View):
|
||||
"""AssertionConsumerService, consume assertion and log user in"""
|
||||
|
||||
def post(self, request: HttpRequest, source_slug: str) -> HttpResponse:
|
||||
def post(self, request: HttpRequest, inlet_slug: str) -> HttpResponse:
|
||||
"""Handles a POSTed SSO Assertion and logs the user in."""
|
||||
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug)
|
||||
if not source.enabled:
|
||||
inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug)
|
||||
if not inlet.enabled:
|
||||
raise Http404
|
||||
processor = Processor(source)
|
||||
processor = Processor(inlet)
|
||||
try:
|
||||
processor.parse(request)
|
||||
except MissingSAMLResponse as exc:
|
||||
|
@ -78,37 +78,34 @@ class ACSView(View):
|
|||
class SLOView(View):
|
||||
"""Single-Logout-View"""
|
||||
|
||||
def dispatch(self, request: HttpRequest, source_slug: str) -> HttpResponse:
|
||||
def dispatch(self, request: HttpRequest, inlet_slug: str) -> HttpResponse:
|
||||
"""Replies with an XHTML SSO Request."""
|
||||
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug)
|
||||
if not source.enabled:
|
||||
inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug)
|
||||
if not inlet.enabled:
|
||||
raise Http404
|
||||
logout(request)
|
||||
return render(
|
||||
request,
|
||||
"saml/sp/sso_single_logout.html",
|
||||
{
|
||||
"idp_logout_url": source.idp_logout_url,
|
||||
"autosubmit": source.auto_logout,
|
||||
},
|
||||
{"idp_logout_url": inlet.idp_logout_url, "autosubmit": inlet.auto_logout,},
|
||||
)
|
||||
|
||||
|
||||
class MetadataView(View):
|
||||
"""Return XML Metadata for IDP"""
|
||||
|
||||
def dispatch(self, request: HttpRequest, source_slug: str) -> HttpResponse:
|
||||
def dispatch(self, request: HttpRequest, inlet_slug: str) -> HttpResponse:
|
||||
"""Replies with the XML Metadata SPSSODescriptor."""
|
||||
source: SAMLSource = get_object_or_404(SAMLSource, slug=source_slug)
|
||||
issuer = get_issuer(request, source)
|
||||
inlet: SAMLInlet = get_object_or_404(SAMLInlet, slug=inlet_slug)
|
||||
issuer = get_issuer(request, inlet)
|
||||
cert_stripped = strip_pem_header(
|
||||
source.signing_kp.certificate_data.replace("\r", "")
|
||||
inlet.signing_kp.certificate_data.replace("\r", "")
|
||||
).replace("\n", "")
|
||||
return render_xml(
|
||||
request,
|
||||
"saml/sp/xml/sp_sso_descriptor.xml",
|
||||
{
|
||||
"acs_url": build_full_url("acs", request, source),
|
||||
"acs_url": build_full_url("acs", request, inlet),
|
||||
"issuer": issuer,
|
||||
"cert_public_key": cert_stripped,
|
||||
},
|
|
@ -1,8 +1,8 @@
|
|||
"""Functions for creating XML output."""
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.channels.out_saml.utils.xml_signing import get_signature_xml
|
||||
from passbook.lib.utils.template import render_to_string
|
||||
from passbook.providers.saml.utils.xml_signing import get_signature_xml
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
"""ApplicationGatewayProvider API Views"""
|
||||
"""ApplicationGatewayOutlet API Views"""
|
||||
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
||||
from oidc_provider.models import Client
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||
from passbook.providers.oidc.api import OpenIDProviderSerializer
|
||||
from passbook.channels.out_app_gw.models import ApplicationGatewayOutlet
|
||||
from passbook.channels.out_oidc.api import OpenIDOutletSerializer
|
||||
|
||||
|
||||
class ApplicationGatewayProviderSerializer(ModelSerializer):
|
||||
"""ApplicationGatewayProvider Serializer"""
|
||||
class ApplicationGatewayOutletSerializer(ModelSerializer):
|
||||
"""ApplicationGatewayOutlet Serializer"""
|
||||
|
||||
client = OpenIDProviderSerializer()
|
||||
client = OpenIDOutletSerializer()
|
||||
|
||||
def create(self, validated_data):
|
||||
instance = super().create(validated_data)
|
||||
|
@ -33,13 +33,13 @@ class ApplicationGatewayProviderSerializer(ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
|
||||
model = ApplicationGatewayProvider
|
||||
model = ApplicationGatewayOutlet
|
||||
fields = ["pk", "name", "internal_host", "external_host", "client"]
|
||||
read_only_fields = ["client"]
|
||||
|
||||
|
||||
class ApplicationGatewayProviderViewSet(ModelViewSet):
|
||||
"""ApplicationGatewayProvider Viewset"""
|
||||
class ApplicationGatewayOutletViewSet(ModelViewSet):
|
||||
"""ApplicationGatewayOutlet Viewset"""
|
||||
|
||||
queryset = ApplicationGatewayProvider.objects.all()
|
||||
serializer_class = ApplicationGatewayProviderSerializer
|
||||
queryset = ApplicationGatewayOutlet.objects.all()
|
||||
serializer_class = ApplicationGatewayOutletSerializer
|
|
@ -5,7 +5,7 @@ from django.apps import AppConfig
|
|||
class PassbookApplicationApplicationGatewayConfig(AppConfig):
|
||||
"""passbook app_gw app"""
|
||||
|
||||
name = "passbook.providers.app_gw"
|
||||
label = "passbook_providers_app_gw"
|
||||
verbose_name = "passbook Providers.Application Security Gateway"
|
||||
name = "passbook.channels.out_app_gw"
|
||||
label = "passbook_channels_out_app_gw"
|
||||
verbose_name = "passbook Outlets.Application Security Gateway"
|
||||
mountpoint = "application/gateway/"
|
|
@ -3,11 +3,11 @@ from django import forms
|
|||
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
||||
from oidc_provider.models import Client, ResponseType
|
||||
|
||||
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||
from passbook.channels.out_app_gw.models import ApplicationGatewayOutlet
|
||||
|
||||
|
||||
class ApplicationGatewayProviderForm(forms.ModelForm):
|
||||
"""Security Gateway Provider form"""
|
||||
class ApplicationGatewayOutletForm(forms.ModelForm):
|
||||
"""Security Gateway Outlet form"""
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.instance.pk:
|
||||
|
@ -31,7 +31,7 @@ class ApplicationGatewayProviderForm(forms.ModelForm):
|
|||
|
||||
class Meta:
|
||||
|
||||
model = ApplicationGatewayProvider
|
||||
model = ApplicationGatewayOutlet
|
||||
fields = ["name", "internal_host", "external_host"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 2.2.7 on 2019-11-11 17:08
|
||||
# Generated by Django 3.0.5 on 2020-05-15 19:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
@ -9,28 +9,28 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0005_merge_20191025_2022"),
|
||||
("passbook_core", "__first__"),
|
||||
("oidc_provider", "0026_client_multiple_response_types"),
|
||||
("passbook_providers_app_gw", "0002_auto_20191111_1703"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ApplicationGatewayProvider",
|
||||
name="ApplicationGatewayOutlet",
|
||||
fields=[
|
||||
(
|
||||
"provider_ptr",
|
||||
"outlet_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_core.Provider",
|
||||
to="passbook_core.Outlet",
|
||||
),
|
||||
),
|
||||
("name", models.TextField()),
|
||||
("host", models.TextField()),
|
||||
("internal_host", models.TextField()),
|
||||
("external_host", models.TextField()),
|
||||
(
|
||||
"client",
|
||||
models.ForeignKey(
|
||||
|
@ -40,9 +40,9 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Application Gateway Provider",
|
||||
"verbose_name_plural": "Application Gateway Providers",
|
||||
"verbose_name": "Application Gateway Outlet",
|
||||
"verbose_name_plural": "Application Gateway Outlets",
|
||||
},
|
||||
bases=("passbook_core.provider",),
|
||||
bases=("passbook_core.outlet",),
|
||||
),
|
||||
]
|
|
@ -9,12 +9,12 @@ from django.utils.translation import gettext as _
|
|||
from oidc_provider.models import Client
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.core.models import Provider
|
||||
from passbook.core.models import Outlet
|
||||
from passbook.lib.utils.template import render_to_string
|
||||
|
||||
|
||||
class ApplicationGatewayProvider(Provider):
|
||||
"""This provider uses oauth2_proxy with the OIDC Provider."""
|
||||
class ApplicationGatewayOutlet(Outlet):
|
||||
"""This outlet uses oauth2_proxy with the OIDC Outlet."""
|
||||
|
||||
name = models.TextField()
|
||||
internal_host = models.TextField()
|
||||
|
@ -22,7 +22,7 @@ class ApplicationGatewayProvider(Provider):
|
|||
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE)
|
||||
|
||||
form = "passbook.providers.app_gw.forms.ApplicationGatewayProviderForm"
|
||||
form = "passbook.channels.out_app_gw.forms.ApplicationGatewayOutletForm"
|
||||
|
||||
def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
|
||||
"""return template and context modal with URLs for authorize, token, openid-config, etc"""
|
||||
|
@ -32,7 +32,7 @@ class ApplicationGatewayProvider(Provider):
|
|||
)
|
||||
return render_to_string(
|
||||
"app_gw/setup_modal.html",
|
||||
{"provider": self, "cookie_secret": cookie_secret, "version": __version__},
|
||||
{"outlet": self, "cookie_secret": cookie_secret, "version": __version__},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -40,5 +40,5 @@ class ApplicationGatewayProvider(Provider):
|
|||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Application Gateway Provider")
|
||||
verbose_name_plural = _("Application Gateway Providers")
|
||||
verbose_name = _("Application Gateway Outlet")
|
||||
verbose_name_plural = _("Application Gateway Outlets")
|
|
@ -42,7 +42,7 @@
|
|||
<h1 class="pf-c-title pf-m-2xl">{% trans 'Setup with Kubernetes' %}</h1>
|
||||
<div class="pf-c-modal-box__body">
|
||||
<p>{% trans 'Download the manifest to create the Gatekeeper deployment and service:' %}</p>
|
||||
<a href="{% url 'passbook_providers_app_gw:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a>
|
||||
<a href="{% url 'passbook_channels_out_app_gw:k8s-manifest' provider=provider.pk %}">{% trans 'Here' %}</a>
|
||||
<p>{% trans 'Afterwards, add the following annotations to the Ingress you want to secure:' %}</p>
|
||||
<textarea class="codemirror" readonly data-cm-mode="yaml">
|
||||
nginx.ingress.kubernetes.io/auth-url: "https://{{ provider.external_host }}/oauth2/auth"
|
|
@ -1,7 +1,7 @@
|
|||
"""passbook app_gw urls"""
|
||||
from django.urls import path
|
||||
|
||||
from passbook.providers.app_gw.views import K8sManifestView
|
||||
from passbook.channels.out_app_gw.views import K8sManifestView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
|
@ -9,7 +9,7 @@ from django.views import View
|
|||
from structlog import get_logger
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||
from passbook.channels.out_app_gw.models import ApplicationGatewayOutlet
|
||||
|
||||
ORIGINAL_URL = "HTTP_X_ORIGINAL_URL"
|
||||
LOGGER = get_logger()
|
||||
|
@ -25,14 +25,14 @@ def get_cookie_secret():
|
|||
class K8sManifestView(LoginRequiredMixin, View):
|
||||
"""Generate K8s Deployment and SVC for gatekeeper"""
|
||||
|
||||
def get(self, request: HttpRequest, provider: int) -> HttpResponse:
|
||||
def get(self, request: HttpRequest, outlet: int) -> HttpResponse:
|
||||
"""Render deployment template"""
|
||||
provider = get_object_or_404(ApplicationGatewayProvider, pk=provider)
|
||||
outlet = get_object_or_404(ApplicationGatewayOutlet, pk=outlet)
|
||||
return render(
|
||||
request,
|
||||
"app_gw/k8s-manifest.yaml",
|
||||
{
|
||||
"provider": provider,
|
||||
"outlet": outlet,
|
||||
"cookie_secret": get_cookie_secret(),
|
||||
"version": __version__,
|
||||
},
|
|
@ -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 passbook.providers.oauth.models import OAuth2Provider
|
||||
from passbook.channels.out_oauth.models import OAuth2Outlet
|
||||
|
||||
|
||||
class OAuth2ProviderForm(forms.ModelForm):
|
||||
"""OAuth2 Provider form"""
|
||||
class OAuth2OutletForm(forms.ModelForm):
|
||||
"""OAuth2 Outlet form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OAuth2Provider
|
||||
model = OAuth2Outlet
|
||||
fields = [
|
||||
"name",
|
||||
"redirect_uris",
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 2.2.6 on 2019-10-07 14:07
|
||||
# Generated by Django 3.0.5 on 2020-05-15 19:59
|
||||
|
||||
import django.db.models.deletion
|
||||
import oauth2_provider.generators
|
||||
|
@ -16,22 +16,22 @@ class Migration(migrations.Migration):
|
|||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("passbook_core", "0001_initial"),
|
||||
("passbook_core", "__first__"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="OAuth2Provider",
|
||||
name="OAuth2Outlet",
|
||||
fields=[
|
||||
(
|
||||
"provider_ptr",
|
||||
"outlet_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_core.Provider",
|
||||
to="passbook_core.Outlet",
|
||||
),
|
||||
),
|
||||
(
|
||||
|
@ -90,15 +90,15 @@ class Migration(migrations.Migration):
|
|||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="passbook_providers_oauth_oauth2provider",
|
||||
related_name="passbook_channels_out_oauth_oauth2outlet",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "OAuth2 Provider",
|
||||
"verbose_name_plural": "OAuth2 Providers",
|
||||
"verbose_name": "OAuth2 Outlet",
|
||||
"verbose_name_plural": "OAuth2 Outlets",
|
||||
},
|
||||
bases=("passbook_core.provider", models.Model),
|
||||
bases=("passbook_core.outlet", models.Model),
|
||||
),
|
||||
]
|
|
@ -7,17 +7,17 @@ from django.shortcuts import reverse
|
|||
from django.utils.translation import gettext as _
|
||||
from oauth2_provider.models import AbstractApplication
|
||||
|
||||
from passbook.core.models import Provider
|
||||
from passbook.core.models import Outlet
|
||||
from passbook.lib.utils.template import render_to_string
|
||||
|
||||
|
||||
class OAuth2Provider(Provider, AbstractApplication):
|
||||
class OAuth2Outlet(Outlet, AbstractApplication):
|
||||
"""Associate an OAuth2 Application with a Product"""
|
||||
|
||||
form = "passbook.providers.oauth.forms.OAuth2ProviderForm"
|
||||
form = "passbook.channels.out_oauth.forms.OAuth2OutletForm"
|
||||
|
||||
def __str__(self):
|
||||
return f"OAuth2 Provider {self.name}"
|
||||
return f"OAuth2 Outlet {self.name}"
|
||||
|
||||
def html_setup_urls(self, request: HttpRequest) -> Optional[str]:
|
||||
"""return template and context modal with URLs for authorize, token, openid-config, etc"""
|
||||
|
@ -26,10 +26,10 @@ class OAuth2Provider(Provider, AbstractApplication):
|
|||
{
|
||||
"provider": self,
|
||||
"authorize_url": request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth:oauth2-authorize")
|
||||
reverse("passbook_channels_out_oauth:oauth2-authorize")
|
||||
),
|
||||
"token_url": request.build_absolute_uri(
|
||||
reverse("passbook_providers_oauth:token")
|
||||
reverse("passbook_channels_out_oauth:token")
|
||||
),
|
||||
"userinfo_url": request.build_absolute_uri(
|
||||
reverse("passbook_api:openid")
|
||||
|
@ -39,5 +39,5 @@ class OAuth2Provider(Provider, AbstractApplication):
|
|||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("OAuth2 Provider")
|
||||
verbose_name_plural = _("OAuth2 Providers")
|
||||
verbose_name = _("OAuth2 Outlet")
|
||||
verbose_name_plural = _("OAuth2 Outlets")
|
|
@ -1,4 +1,4 @@
|
|||
"""passbook OAuth_Provider"""
|
||||
"""passbook OAuth_Outlet"""
|
||||
from django.conf import settings
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = settings.DEBUG
|
||||
|
@ -17,7 +17,7 @@ AUTHENTICATION_BACKENDS = [
|
|||
"oauth2_provider.backends.OAuth2Backend",
|
||||
]
|
||||
|
||||
OAUTH2_PROVIDER_APPLICATION_MODEL = "passbook_providers_oauth.OAuth2Provider"
|
||||
OAUTH2_PROVIDER_APPLICATION_MODEL = "passbook_channels_out_oauth.OAuth2Outlet"
|
||||
|
||||
OAUTH2_PROVIDER = {
|
||||
# this is the list of available scopes
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue