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

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

View file

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

View file

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

View file

@ -8,12 +8,12 @@ from passbook.admin.views import (
debug,
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"),

View file

@ -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):

View file

@ -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):

View file

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

View file

@ -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):

View file

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

View file

@ -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

View file

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

View file

@ -1,4 +1,4 @@
# Generated by Django 2.2.6 on 2019-10-07 14:07
# Generated by Django 3.0.5 on 2020-05-15 19:58
import uuid
@ -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",
},
),
]

View file

@ -1,16 +0,0 @@
# Generated by Django 2.2.6 on 2019-10-28 08:29
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("passbook_audit", "0001_initial"),
]
operations = [
migrations.RenameModel(old_name="AuditEntry", new_name="Event",),
]

View file

@ -1,40 +0,0 @@
# Generated by Django 2.2.8 on 2019-12-05 14:07
from django.db import migrations, models
import passbook.audit.models
class Migration(migrations.Migration):
dependencies = [
("passbook_audit", "0002_auto_20191028_0829"),
]
operations = [
migrations.AlterModelOptions(
name="event",
options={
"verbose_name": "Audit Event",
"verbose_name_plural": "Audit Events",
},
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("LOGIN", "login"),
("LOGIN_FAILED", "login_failed"),
("LOGOUT", "logout"),
("AUTHORIZE_APPLICATION", "authorize_application"),
("SUSPICIOUS_REQUEST", "suspicious_request"),
("SIGN_UP", "sign_up"),
("PASSWORD_RESET", "password_reset"),
("INVITE_CREATED", "invitation_created"),
("INVITE_USED", "invitation_used"),
("CUSTOM", "custom"),
]
),
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 2.2.8 on 2019-12-05 15:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_audit", "0003_auto_20191205_1407"),
]
operations = [
migrations.RemoveField(model_name="event", name="request_ip",),
migrations.AddField(
model_name="event",
name="client_ip",
field=models.GenericIPAddressField(null=True),
),
]

View file

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

View file

@ -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):

View file

@ -0,0 +1,11 @@
"""Passbook ldap app config"""
from django.apps import AppConfig
class PassbookInletLDAPConfig(AppConfig):
"""Passbook ldap app config"""
name = "passbook.channels.in_ldap"
label = "passbook_channels_in_ldap"
verbose_name = "passbook Inlets.LDAP"

View file

@ -3,8 +3,8 @@ from django.contrib.auth.backends import ModelBackend
from django.http import HttpRequest
from 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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
# Generated by Django 2.2.6 on 2019-10-08 20:43
# Generated by Django 3.0.5 on 2020-05-15 19:59
import django.core.validators
import django.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",),
),
]

View file

@ -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}"

View file

@ -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
}
}

View file

@ -0,0 +1,33 @@
"""LDAP Sync tasks"""
from passbook.channels.in_ldap.connector import Connector
from passbook.channels.in_ldap.models import LDAPInlet
from passbook.root.celery import CELERY_APP
@CELERY_APP.task()
def sync_groups(inlet_pk: int):
"""Sync LDAP Groups on background worker"""
inlet = LDAPInlet.objects.get(pk=inlet_pk)
connector = Connector(inlet)
connector.bind()
connector.sync_groups()
@CELERY_APP.task()
def sync_users(inlet_pk: int):
"""Sync LDAP Users on background worker"""
inlet = LDAPInlet.objects.get(pk=inlet_pk)
connector = Connector(inlet)
connector.bind()
connector.sync_users()
@CELERY_APP.task()
def sync():
"""Sync all inlets"""
for inlet in LDAPInlet.objects.filter(enabled=True):
connector = Connector(inlet)
connector.bind()
connector.sync_users()
connector.sync_groups()
connector.sync_membership()

View file

@ -0,0 +1,29 @@
"""OAuth Inlet Serializer"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.admin.forms.inlet import INLET_SERIALIZER_FIELDS
from passbook.channels.in_oauth.models import OAuthInlet
class OAuthInletSerializer(ModelSerializer):
"""OAuth Inlet Serializer"""
class Meta:
model = OAuthInlet
fields = INLET_SERIALIZER_FIELDS + [
"inlet_type",
"request_token_url",
"authorization_url",
"access_token_url",
"profile_url",
"consumer_key",
"consumer_secret",
]
class OAuthInletViewSet(ModelViewSet):
"""Inlet Viewset"""
queryset = OAuthInlet.objects.all()
serializer_class = OAuthInletSerializer

View file

@ -8,12 +8,12 @@ from structlog import get_logger
LOGGER = get_logger()
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):

View file

@ -0,0 +1,24 @@
"""passbook oauth_client Authorization backend"""
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from passbook.channels.in_oauth.models import OAuthInlet, UserOAuthInletConnection
class AuthorizedServiceBackend(ModelBackend):
"Authentication backend for users registered with remote OAuth provider."
def authenticate(self, request, inlet=None, identifier=None):
"Fetch user for a given inlet by id."
inlet_q = Q(inlet__name=inlet)
if isinstance(inlet, OAuthInlet):
inlet_q = Q(inlet=inlet)
try:
access = UserOAuthInletConnection.objects.filter(
inlet_q, identifier=identifier
).select_related("user")[0]
except IndexError:
return None
else:
return access.user

View file

@ -21,8 +21,8 @@ class BaseOAuthClient:
session: Session = None
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)

View file

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

View file

@ -0,0 +1,81 @@
# Generated by Django 3.0.5 on 2020-05-15 19:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("passbook_core", "__first__"),
]
operations = [
migrations.CreateModel(
name="OAuthInlet",
fields=[
(
"inlet_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_core.Inlet",
),
),
("inlet_type", models.CharField(max_length=255)),
(
"request_token_url",
models.CharField(
blank=True, max_length=255, verbose_name="Request Token URL"
),
),
(
"authorization_url",
models.CharField(max_length=255, verbose_name="Authorization URL"),
),
(
"access_token_url",
models.CharField(max_length=255, verbose_name="Access Token URL"),
),
(
"profile_url",
models.CharField(max_length=255, verbose_name="Profile URL"),
),
("consumer_key", models.TextField()),
("consumer_secret", models.TextField()),
],
options={
"verbose_name": "Generic OAuth Inlet",
"verbose_name_plural": "Generic OAuth Inlets",
},
bases=("passbook_core.inlet",),
),
migrations.CreateModel(
name="UserOAuthInletConnection",
fields=[
(
"userinletconnection_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_core.UserInletConnection",
),
),
("identifier", models.CharField(max_length=255)),
("access_token", models.TextField(blank=True, default=None, null=True)),
],
options={
"verbose_name": "User OAuth Inlet Connection",
"verbose_name_plural": "User OAuth Inlet Connections",
},
bases=("passbook_core.userinletconnection",),
),
]

View file

@ -0,0 +1,159 @@
"""OAuth Client models"""
from django.db import models
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from passbook.channels.in_oauth.clients import get_client
from passbook.core.models import Inlet, UserInletConnection
from passbook.core.types import UILoginButton, UIUserSettings
class OAuthInlet(Inlet):
"""Configuration for OAuth inlet."""
inlet_type = models.CharField(max_length=255)
request_token_url = models.CharField(
blank=True, max_length=255, verbose_name=_("Request Token URL")
)
authorization_url = models.CharField(
max_length=255, verbose_name=_("Authorization URL")
)
access_token_url = models.CharField(
max_length=255, verbose_name=_("Access Token URL")
)
profile_url = models.CharField(max_length=255, verbose_name=_("Profile URL"))
consumer_key = models.TextField()
consumer_secret = models.TextField()
form = "passbook.channels.in_oauth.forms.OAuthInletForm"
@property
def ui_login_button(self) -> UILoginButton:
return UILoginButton(
url=reverse_lazy(
"passbook_channels_in_oauth:oauth-client-login",
kwargs={"inlet_slug": self.slug},
),
icon_path=f"passbook/inlets/{self.inlet_type}.svg",
name=self.name,
)
@property
def ui_additional_info(self) -> str:
url = reverse_lazy(
"passbook_channels_in_oauth:oauth-client-callback",
kwargs={"inlet_slug": self.slug},
)
return f"Callback URL: <pre>{url}</pre>"
@property
def ui_user_settings(self) -> UIUserSettings:
icon_type = self.inlet_type
if icon_type == "azure ad":
icon_type = "windows"
icon_class = f"fab fa-{icon_type}"
view_name = "passbook_channels_in_oauth:oauth-client-user"
return UIUserSettings(
name=self.name,
icon=icon_class,
view_name=reverse((view_name), kwargs={"inlet_slug": self.slug}),
)
class Meta:
verbose_name = _("Generic OAuth Inlet")
verbose_name_plural = _("Generic OAuth Inlets")
class GitHubOAuthInlet(OAuthInlet):
"""Abstract subclass of OAuthInlet to specify GitHub Form"""
form = "passbook.channels.in_oauth.forms.GitHubOAuthInletForm"
class Meta:
abstract = True
verbose_name = _("GitHub OAuth Inlet")
verbose_name_plural = _("GitHub OAuth Inlets")
class TwitterOAuthInlet(OAuthInlet):
"""Abstract subclass of OAuthInlet to specify Twitter Form"""
form = "passbook.channels.in_oauth.forms.TwitterOAuthInletForm"
class Meta:
abstract = True
verbose_name = _("Twitter OAuth Inlet")
verbose_name_plural = _("Twitter OAuth Inlets")
class FacebookOAuthInlet(OAuthInlet):
"""Abstract subclass of OAuthInlet to specify Facebook Form"""
form = "passbook.channels.in_oauth.forms.FacebookOAuthInletForm"
class Meta:
abstract = True
verbose_name = _("Facebook OAuth Inlet")
verbose_name_plural = _("Facebook OAuth Inlets")
class DiscordOAuthInlet(OAuthInlet):
"""Abstract subclass of OAuthInlet to specify Discord Form"""
form = "passbook.channels.in_oauth.forms.DiscordOAuthInletForm"
class Meta:
abstract = True
verbose_name = _("Discord OAuth Inlet")
verbose_name_plural = _("Discord OAuth Inlets")
class GoogleOAuthInlet(OAuthInlet):
"""Abstract subclass of OAuthInlet to specify Google Form"""
form = "passbook.channels.in_oauth.forms.GoogleOAuthInletForm"
class Meta:
abstract = True
verbose_name = _("Google OAuth Inlet")
verbose_name_plural = _("Google OAuth Inlets")
class AzureADOAuthInlet(OAuthInlet):
"""Abstract subclass of OAuthInlet to specify AzureAD Form"""
form = "passbook.channels.in_oauth.forms.AzureADOAuthInletForm"
class Meta:
abstract = True
verbose_name = _("Azure AD OAuth Inlet")
verbose_name_plural = _("Azure AD OAuth Inlets")
class UserOAuthInletConnection(UserInletConnection):
"""Authorized remote OAuth inlet."""
identifier = models.CharField(max_length=255)
access_token = models.TextField(blank=True, null=True, default=None)
def save(self, *args, **kwargs):
self.access_token = self.access_token or None
super().save(*args, **kwargs)
@property
def api_client(self):
"""Get API Client"""
return get_client(self.inlet, self.access_token or "")
class Meta:
verbose_name = _("User OAuth Inlet Connection")
verbose_name_plural = _("User OAuth Inlet Connections")

View file

@ -0,0 +1,15 @@
"""Oauth2 Client Settings"""
AUTHENTICATION_BACKENDS = [
"passbook.channels.in_oauth.backends.AuthorizedServiceBackend",
]
PASSBOOK_SOURCES_OAUTH_TYPES = [
"passbook.channels.in_oauth.types.discord",
"passbook.channels.in_oauth.types.facebook",
"passbook.channels.in_oauth.types.github",
"passbook.channels.in_oauth.types.google",
"passbook.channels.in_oauth.types.reddit",
"passbook.channels.in_oauth.types.twitter",
"passbook.channels.in_oauth.types.azure_ad",
]

View file

@ -9,12 +9,12 @@
<div class="pf-c-card__body">
{% 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 %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,28 @@
"""SAMLInlet API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.channels.in_saml.models import SAMLInlet
class SAMLInletSerializer(ModelSerializer):
"""SAMLInlet Serializer"""
class Meta:
model = SAMLInlet
fields = [
"pk",
"issuer",
"idp_url",
"idp_logout_url",
"auto_logout",
"signing_kp",
]
class SAMLInletViewSet(ModelViewSet):
"""SAMLInlet Viewset"""
queryset = SAMLInlet.objects.all()
serializer_class = SAMLInletSerializer

View file

@ -0,0 +1,12 @@
"""Passbook SAML app config"""
from django.apps import AppConfig
class PassbookInletSAMLConfig(AppConfig):
"""passbook saml_idp app config"""
name = "passbook.channels.in_saml"
label = "passbook_channels_in_saml"
verbose_name = "passbook Inlets.SAML"
mountpoint = "source/saml/"

View file

@ -4,17 +4,17 @@ from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.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",

View file

@ -0,0 +1,68 @@
# Generated by Django 3.0.5 on 2020-05-15 19:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("passbook_crypto", "0001_initial"),
("passbook_core", "__first__"),
]
operations = [
migrations.CreateModel(
name="SAMLInlet",
fields=[
(
"inlet_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_core.Inlet",
),
),
(
"issuer",
models.TextField(
blank=True,
default=None,
help_text="Also known as Entity ID. Defaults the Metadata URL.",
verbose_name="Issuer",
),
),
("idp_url", models.URLField(verbose_name="IDP URL")),
(
"idp_logout_url",
models.URLField(
blank=True,
default=None,
null=True,
verbose_name="IDP Logout URL",
),
),
("auto_logout", models.BooleanField(default=False)),
(
"signing_kp",
models.ForeignKey(
default=None,
help_text="Certificate Key Pair of the IdP which Assertions are validated against.",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="passbook_crypto.CertificateKeyPair",
),
),
],
options={
"verbose_name": "SAML Inlet",
"verbose_name_plural": "SAML Inlets",
},
bases=("passbook_core.inlet",),
),
]

View file

@ -3,13 +3,13 @@ from django.db import models
from django.urls import reverse_lazy
from django.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")

View file

@ -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]:
"""

View file

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

View file

@ -0,0 +1,20 @@
"""saml sp helpers"""
from django.http import HttpRequest
from django.shortcuts import reverse
from passbook.channels.in_saml.models import SAMLInlet
def get_issuer(request: HttpRequest, inlet: SAMLInlet) -> str:
"""Get Inlet's Issuer, falling back to our Metadata URL if none is set"""
issuer = inlet.issuer
if issuer is None:
return build_full_url("metadata", request, inlet)
return issuer
def build_full_url(view: str, request: HttpRequest, inlet: SAMLInlet) -> str:
"""Build Full ACS URL to be used in IDP"""
return request.build_absolute_uri(
reverse(f"passbook_channels_in_saml:{view}", kwargs={"inlet_slug": inlet.slug})
)

View file

@ -7,36 +7,36 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from 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,
},

View file

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

View file

@ -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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
# Generated by Django 2.2.7 on 2019-11-11 17:08
# Generated by Django 3.0.5 on 2020-05-15 19:59
import django.db.models.deletion
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",),
),
]

View file

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

View file

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

View file

@ -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(

View file

@ -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__,
},

View file

@ -0,0 +1,29 @@
"""OAuth2Outlet API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.channels.out_oauth.models import OAuth2Outlet
class OAuth2OutletSerializer(ModelSerializer):
"""OAuth2Outlet Serializer"""
class Meta:
model = OAuth2Outlet
fields = [
"pk",
"name",
"redirect_uris",
"client_type",
"authorization_grant_type",
"client_id",
"client_secret",
]
class OAuth2OutletViewSet(ModelViewSet):
"""OAuth2Outlet Viewset"""
queryset = OAuth2Outlet.objects.all()
serializer_class = OAuth2OutletSerializer

View file

@ -0,0 +1,12 @@
"""passbook auth oauth provider app config"""
from django.apps import AppConfig
class PassbookOutletOAuthConfig(AppConfig):
"""passbook auth oauth provider app config"""
name = "passbook.channels.out_oauth"
label = "passbook_channels_out_oauth"
verbose_name = "passbook Outlets.OAuth"
mountpoint = ""

View file

@ -1,16 +1,16 @@
"""passbook OAuth2 Provider Forms"""
"""passbook OAuth2 Outlet Forms"""
from django import forms
from 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",

View file

@ -1,4 +1,4 @@
# Generated by Django 2.2.6 on 2019-10-07 14:07
# Generated by Django 3.0.5 on 2020-05-15 19:59
import django.db.models.deletion
import 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),
),
]

View file

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

View file

@ -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