From c1276e969503d868ea8a479cb6958488ad42d6e0 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 16 Nov 2018 11:41:14 +0100 Subject: [PATCH] redo models again --- passbook/core/migrations/0001_initial.py | 31 +++++-- passbook/core/models.py | 8 ++ passbook/core/settings.py | 2 +- passbook/core/templates/base/skeleton.html | 7 +- passbook/core/urls.py | 9 +- passbook/core/views/authentication.py | 3 +- passbook/core/views/overview.py | 4 + passbook/lib/templatetags/utils.py | 2 +- .../oauth_client/migrations/0001_initial.py | 6 +- .../oauth_provider/migrations/0001_initial.py | 17 ++-- passbook/oauth_provider/models.py | 8 +- passbook/oauth_provider/urls.py | 2 +- passbook/saml_idp/migrations/0001_initial.py | 29 ++++++ passbook/saml_idp/models.py | 4 +- passbook/saml_idp/registry.py | 4 +- passbook/saml_idp/urls.py | 2 +- passbook/saml_idp/views.py | 89 ++++++++----------- 17 files changed, 139 insertions(+), 88 deletions(-) create mode 100644 passbook/saml_idp/migrations/0001_initial.py diff --git a/passbook/core/migrations/0001_initial.py b/passbook/core/migrations/0001_initial.py index ccf8d33ca..ebdafdf3b 100644 --- a/passbook/core/migrations/0001_initial.py +++ b/passbook/core/migrations/0001_initial.py @@ -1,13 +1,12 @@ -# Generated by Django 2.1.3 on 2018-11-11 14:06 - -import uuid +# Generated by Django 2.1.3 on 2018-11-16 10:21 +from django.conf import settings import django.contrib.auth.models import django.contrib.auth.validators +from django.db import migrations, models import django.db.models.deletion import django.utils.timezone -from django.conf import settings -from django.db import migrations, models +import uuid class Migration(migrations.Migration): @@ -33,7 +32,6 @@ class Migration(migrations.Migration): ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), ], options={ 'verbose_name': 'user', @@ -58,6 +56,12 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='Provider', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), migrations.CreateModel( name='Rule', fields=[ @@ -114,6 +118,21 @@ class Migration(migrations.Migration): name='application', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Application'), ), + migrations.AddField( + model_name='application', + name='provider', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_core.Provider'), + ), + migrations.AddField( + model_name='user', + name='applications', + field=models.ManyToManyField(to='passbook_core.Application'), + ), + migrations.AddField( + model_name='user', + name='groups', + field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), + ), migrations.AddField( model_name='user', name='sources', diff --git a/passbook/core/models.py b/passbook/core/models.py index 7df180182..f5b757e3f 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -16,6 +16,13 @@ class User(AbstractUser): """Custom User model to allow easier adding o f user-based settings""" sources = models.ManyToManyField('Source', through='UserSourceConnection') + applications = models.ManyToManyField('Application') + +@reversion.register() +class Provider(models.Model): + """Application-independant Provider instance. For example SAML2 Remote, OAuth2 Application""" + + # This class defines no field for easier inheritance @reversion.register() class Application(UUIDModel, CreatedUpdatedModel): @@ -26,6 +33,7 @@ class Application(UUIDModel, CreatedUpdatedModel): name = models.TextField() launch_url = models.URLField(null=True, blank=True) icon_url = models.TextField(null=True, blank=True) + provider = models.ForeignKey('Provider', null=True, default=None, on_delete=models.SET_DEFAULT) objects = InheritanceManager() diff --git a/passbook/core/settings.py b/passbook/core/settings.py index f788ea706..56b2c3246 100644 --- a/passbook/core/settings.py +++ b/passbook/core/settings.py @@ -53,9 +53,9 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'reversion', + 'rest_framework', 'passbook.core', 'passbook.admin', - 'rest_framework', 'passbook.lib', 'passbook.ldap', 'passbook.oauth_client', diff --git a/passbook/core/templates/base/skeleton.html b/passbook/core/templates/base/skeleton.html index ada325097..7506d4db6 100644 --- a/passbook/core/templates/base/skeleton.html +++ b/passbook/core/templates/base/skeleton.html @@ -1,11 +1,16 @@ {% load static %} {% load i18n %} +{% load utils %} - {% trans 'passbook' %} + + {% block title %} + {% title %} + {% endblock %} + diff --git a/passbook/core/urls.py b/passbook/core/urls.py index e97abd581..230923882 100644 --- a/passbook/core/urls.py +++ b/passbook/core/urls.py @@ -24,9 +24,12 @@ urlpatterns = [ include(('passbook.admin.urls', 'passbook_admin'), namespace='passbook_admin')), path('source/oauth/', include(('passbook.oauth_client.urls', 'passbook_oauth_client'), namespace='passbook_oauth_client')), - path('application/oauth', include(('passbook.oauth_provider.urls', - 'passbook_oauth_provider'), - namespace='passbook_oauth_provider')), + path('application/oauth/', include(('passbook.oauth_provider.urls', + 'passbook_oauth_provider'), + namespace='passbook_oauth_provider')), + path('application/saml/', include(('passbook.saml_idp.urls', + 'passbook_saml_idp'), + namespace='passbook_saml_idp')), ] if settings.DEBUG: diff --git a/passbook/core/views/authentication.py b/passbook/core/views/authentication.py index ff1528dd3..e8b0f448b 100644 --- a/passbook/core/views/authentication.py +++ b/passbook/core/views/authentication.py @@ -75,7 +75,7 @@ class LoginView(UserPassesTestMixin, FormView): login(request, user) if cleaned_data.get('remember') is True: - request.session.set_expiry(CONFIG.get('passbook').get('session').get('remember_age')) + request.session.set_expiry(CONFIG.y('passbook.session.remember_age')) else: request.session.set_expiry(0) # Expires when browser is closed messages.success(request, _("Successfully logged in!")) @@ -98,4 +98,5 @@ class LoginView(UserPassesTestMixin, FormView): context = { 'reason': 'invalid', } + raise NotImplementedError() return render(request, 'login/invalid.html', context) diff --git a/passbook/core/views/overview.py b/passbook/core/views/overview.py index 7dbf04a12..2ab835e28 100644 --- a/passbook/core/views/overview.py +++ b/passbook/core/views/overview.py @@ -9,3 +9,7 @@ class OverviewView(LoginRequiredMixin, TemplateView): and is not being forwarded""" template_name = 'overview/index.html' + + def get_context_data(self, **kwargs): + kwargs['applications'] = self.request.user.applications.objects.all() + return super().get_context_data(**kwargs) diff --git a/passbook/lib/templatetags/utils.py b/passbook/lib/templatetags/utils.py index 6c3114e5f..ec1b9b7e2 100644 --- a/passbook/lib/templatetags/utils.py +++ b/passbook/lib/templatetags/utils.py @@ -76,7 +76,7 @@ def pick(cont, arg, fallback=''): @register.simple_tag(takes_context=True) def title(context, *title): """Return either just branding or title - branding""" - branding = Setting.get('branding', default='passbook') + branding = CONFIG.y('passbook.branding', 'passbook') if not title: return branding # Include App Title in title diff --git a/passbook/oauth_client/migrations/0001_initial.py b/passbook/oauth_client/migrations/0001_initial.py index 86d56ac5d..0b832aed5 100644 --- a/passbook/oauth_client/migrations/0001_initial.py +++ b/passbook/oauth_client/migrations/0001_initial.py @@ -1,7 +1,7 @@ -# Generated by Django 2.1.3 on 2018-11-11 14:06 +# Generated by Django 2.1.3 on 2018-11-16 10:21 -import django.db.models.deletion from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -9,7 +9,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('passbook_core', '0001_initial'), + ('passbook_core', '__first__'), ] operations = [ diff --git a/passbook/oauth_provider/migrations/0001_initial.py b/passbook/oauth_provider/migrations/0001_initial.py index 9e69feab0..23e3cc2d3 100644 --- a/passbook/oauth_provider/migrations/0001_initial.py +++ b/passbook/oauth_provider/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# Generated by Django 2.1.3 on 2018-11-14 18:35 +# Generated by Django 2.1.3 on 2018-11-16 10:21 -import django.db.models.deletion from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -11,19 +11,16 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL), - ('passbook_core', '0001_initial'), + ('passbook_core', '__first__'), ] operations = [ migrations.CreateModel( - name='OAuth2Application', + name='OAuth2Provider', fields=[ - ('application_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Application')), - ('oauth2', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), + ('provider_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')), + ('oauth2_app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), ], - options={ - 'abstract': False, - }, - bases=('passbook_core.application',), + bases=('passbook_core.provider',), ), ] diff --git a/passbook/oauth_provider/models.py b/passbook/oauth_provider/models.py index dde4cf0f6..24ae516f4 100644 --- a/passbook/oauth_provider/models.py +++ b/passbook/oauth_provider/models.py @@ -1,12 +1,12 @@ """Oauth2 provider product extension""" from django.db import models -from oauth2_provider.models import Application as _OAuth2Application +from oauth2_provider.models import Application -from passbook.core.models import Application +from passbook.core.models import Provider -class OAuth2Application(Application): +class OAuth2Provider(Provider): """Associate an OAuth2 Application with a Product""" - oauth2 = models.ForeignKey(_OAuth2Application, on_delete=models.CASCADE) + oauth2_app = models.ForeignKey(Application, on_delete=models.CASCADE) diff --git a/passbook/oauth_provider/urls.py b/passbook/oauth_provider/urls.py index 41bc75807..6d78d9b59 100644 --- a/passbook/oauth_provider/urls.py +++ b/passbook/oauth_provider/urls.py @@ -8,5 +8,5 @@ urlpatterns = [ # Custom OAuth 2 Authorize View # path('authorize/', oauth2.PassbookAuthorizationView.as_view(), name="oauth2-authorize"), # OAuth API - path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')), + path('', include('oauth2_provider.urls', namespace='oauth2_provider')), ] diff --git a/passbook/saml_idp/migrations/0001_initial.py b/passbook/saml_idp/migrations/0001_initial.py new file mode 100644 index 000000000..20b0bb55b --- /dev/null +++ b/passbook/saml_idp/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 2.1.3 on 2018-11-16 10:21 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('passbook_core', '__first__'), + ] + + operations = [ + migrations.CreateModel( + name='SAMLApplication', + fields=[ + ('application_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Application')), + ('acs_url', models.URLField()), + ('processor_path', models.CharField(max_length=255)), + ('skip_authorization', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + bases=('passbook_core.application',), + ), + ] diff --git a/passbook/saml_idp/models.py b/passbook/saml_idp/models.py index 0a67908b9..7d03066ea 100644 --- a/passbook/saml_idp/models.py +++ b/passbook/saml_idp/models.py @@ -7,7 +7,7 @@ from passbook.lib.utils.reflection import class_to_path from passbook.saml_idp.base import Processor -class SAMLRemote(Application): +class SAMLApplication(Application): """Model to save information about a Remote SAML Endpoint""" acs_url = models.URLField() @@ -20,4 +20,4 @@ class SAMLRemote(Application): self._meta.get_field('processor_path').choices = processors def __str__(self): - return "SAMLRemote %s (processor=%s)" % (self.name, self.processor_path) + return "SAMLApplication %s (processor=%s)" % (self.name, self.processor_path) diff --git a/passbook/saml_idp/registry.py b/passbook/saml_idp/registry.py index 832aeafca..b283070b8 100644 --- a/passbook/saml_idp/registry.py +++ b/passbook/saml_idp/registry.py @@ -3,7 +3,7 @@ from logging import getLogger from passbook.lib.utils.reflection import path_to_class from passbook.saml_idp.exceptions import CannotHandleAssertion -from passbook.saml_idp.models import SAMLRemote +from passbook.saml_idp.models import SAMLApplication LOGGER = getLogger(__name__) @@ -16,7 +16,7 @@ def get_processor(remote): def find_processor(request): """Returns the Processor instance that is willing to handle this request.""" - for remote in SAMLRemote.objects.all(): + for remote in SAMLApplication.objects.all(): proc = get_processor(remote) try: if proc.can_handle(request): diff --git a/passbook/saml_idp/urls.py b/passbook/saml_idp/urls.py index 27b4e010b..00bfc91d6 100644 --- a/passbook/saml_idp/urls.py +++ b/passbook/saml_idp/urls.py @@ -8,5 +8,5 @@ urlpatterns = [ url(r'^login/process/$', views.login_process, name='saml_login_process'), url(r'^logout/$', views.logout, name="saml_logout"), url(r'^metadata/xml/$', views.descriptor, name='metadata_xml'), - url(r'^settings/$', views.IDPSettingsView.as_view(), name='admin_settings'), + # url(r'^settings/$', views.IDPSettingsView.as_view(), name='admin_settings'), ] diff --git a/passbook/saml_idp/views.py b/passbook/saml_idp/views.py index d80873a97..19df34218 100644 --- a/passbook/saml_idp/views.py +++ b/passbook/saml_idp/views.py @@ -17,22 +17,19 @@ from OpenSSL.crypto import FILETYPE_PEM from OpenSSL.crypto import Error as CryptoError from OpenSSL.crypto import load_certificate -from passbook.core.models import Event, Setting, UserAcquirableRelationship -from passbook.core.utils import render_to_string -from passbook.core.views.common import ErrorResponseView -from passbook.core.views.settings import GenericSettingView -from passbook.mod.auth.saml.idp import exceptions, registry, xml_signing -from passbook.mod.auth.saml.idp.forms.settings import IDPSettingsForm +# from passbook.core.models import Event, Setting, UserAcquirableRelationship +from passbook.lib.utils.template import render_to_string +# from passbook.core.views.common import ErrorResponseView +# from passbook.core.views.settings import GenericSettingView +from passbook.saml_idp import exceptions, registry, xml_signing LOGGER = getLogger(__name__) URL_VALIDATOR = URLValidator(schemes=('http', 'https')) def _generate_response(request, processor, remote): - """ - Generate a SAML response using processor and return it in the proper Django - response. - """ + """Generate a SAML response using processor and return it in the proper Django + response.""" try: ctx = processor.generate_response() ctx['remote'] = remote @@ -49,10 +46,8 @@ def render_xml(request, template, ctx): @csrf_exempt def login_begin(request): - """ - Receives a SAML 2.0 AuthnRequest from a Service Provider and - stores it in the session prior to enforcing login. - """ + """Receives a SAML 2.0 AuthnRequest from a Service Provider and + stores it in the session prior to enforcing login.""" if request.method == 'POST': source = request.POST else: @@ -65,13 +60,11 @@ def login_begin(request): return HttpResponseBadRequest('the SAML request payload is missing') request.session['RelayState'] = source.get('RelayState', '') - return redirect(reverse('passbook_mod_auth_saml_idp:saml_login_process')) + return redirect(reverse('passbook_saml_idp:saml_login_process')) def redirect_to_sp(request, acs_url, saml_response, relay_state): - """ - Return autosubmit form - """ + """Return autosubmit form""" return render(request, 'core/autosubmit_form.html', { 'url': acs_url, 'attrs': { @@ -83,10 +76,8 @@ def redirect_to_sp(request, acs_url, saml_response, relay_state): @login_required def login_process(request): - """ - Processor-based login continuation. - Presents a SAML 2.0 Assertion for POSTing back to the Service Provider. - """ + """Processor-based login continuation. + Presents a SAML 2.0 Assertion for POSTing back to the Service Provider.""" LOGGER.debug("Request: %s", request) proc, remote = registry.find_processor(request) # Check if user has access @@ -141,11 +132,9 @@ def login_process(request): @csrf_exempt def logout(request): - """ - Allows a non-SAML 2.0 URL to log out the user and + """Allows a non-SAML 2.0 URL to log out the user and returns a standard logged-out page. (SalesForce and others use this method, - though it's technically not SAML 2.0). - """ + though it's technically not SAML 2.0).""" auth.logout(request) redirect_url = request.GET.get('redirect_to', '') @@ -163,10 +152,8 @@ def logout(request): @login_required @csrf_exempt def slo_logout(request): - """ - Receives a SAML 2.0 LogoutRequest from a Service Provider, - logs out the user and returns a standard logged-out page. - """ + """Receives a SAML 2.0 LogoutRequest from a Service Provider, + logs out the user and returns a standard logged-out page.""" request.session['SAMLRequest'] = request.POST['SAMLRequest'] # TODO: Parse SAML LogoutRequest from POST data, similar to login_process(). # TODO: Add a URL dispatch for this view. @@ -179,12 +166,10 @@ def slo_logout(request): def descriptor(request): - """ - Replies with the XML Metadata IDSSODescriptor. - """ - entity_id = Setting.get('issuer') - slo_url = request.build_absolute_uri(reverse('passbook_mod_auth_saml_idp:saml_logout')) - sso_url = request.build_absolute_uri(reverse('passbook_mod_auth_saml_idp:saml_login_begin')) + """Replies with the XML Metadata IDSSODescriptor.""" + entity_id = CONFIG.y('saml_idp.issuer') + slo_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_logout')) + sso_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_login_begin')) pubkey = xml_signing.load_certificate(strip=True) ctx = { 'entity_id': entity_id, @@ -194,25 +179,25 @@ def descriptor(request): } metadata = render_to_string('saml/xml/metadata.xml', ctx) response = HttpResponse(metadata, content_type='application/xml') - response['Content-Disposition'] = 'attachment; filename="sv_metadata.xml' + response['Content-Disposition'] = 'attachment; filename="passbook_metadata.xml' return response -class IDPSettingsView(GenericSettingView): - """IDP Settings""" +# class IDPSettingsView(GenericSettingView): +# """IDP Settings""" - form = IDPSettingsForm - template_name = 'saml/idp/settings.html' +# form = IDPSettingsForm +# template_name = 'saml/idp/settings.html' - def dispatch(self, request, *args, **kwargs): - self.extra_data['metadata'] = escape(descriptor(request).content.decode('utf-8')) +# def dispatch(self, request, *args, **kwargs): +# self.extra_data['metadata'] = escape(descriptor(request).content.decode('utf-8')) - # Show the certificate fingerprint - sha1_fingerprint = _('') - try: - cert = load_certificate(FILETYPE_PEM, Setting.get('certificate')) - sha1_fingerprint = cert.digest("sha1") - except CryptoError: - pass - self.extra_data['fingerprint'] = sha1_fingerprint - return super().dispatch(request, *args, **kwargs) +# # Show the certificate fingerprint +# sha1_fingerprint = _('') +# try: +# cert = load_certificate(FILETYPE_PEM, CONFIG.y('saml_idp.certificate')) +# sha1_fingerprint = cert.digest("sha1") +# except CryptoError: +# pass +# self.extra_data['fingerprint'] = sha1_fingerprint +# return super().dispatch(request, *args, **kwargs)