redo models again

This commit is contained in:
Jens Langhammer 2018-11-16 11:41:14 +01:00
parent de7a2fa034
commit c1276e9695
17 changed files with 139 additions and 88 deletions

View File

@ -1,13 +1,12 @@
# 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 uuid
from django.conf import settings
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
from django.conf import settings import uuid
from django.db import migrations, models
class Migration(migrations.Migration): 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_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')), ('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')), ('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={ options={
'verbose_name': 'user', 'verbose_name': 'user',
@ -58,6 +56,12 @@ class Migration(migrations.Migration):
'abstract': False, 'abstract': False,
}, },
), ),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel( migrations.CreateModel(
name='Rule', name='Rule',
fields=[ fields=[
@ -114,6 +118,21 @@ class Migration(migrations.Migration):
name='application', name='application',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.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( migrations.AddField(
model_name='user', model_name='user',
name='sources', name='sources',

View File

@ -16,6 +16,13 @@ class User(AbstractUser):
"""Custom User model to allow easier adding o f user-based settings""" """Custom User model to allow easier adding o f user-based settings"""
sources = models.ManyToManyField('Source', through='UserSourceConnection') 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() @reversion.register()
class Application(UUIDModel, CreatedUpdatedModel): class Application(UUIDModel, CreatedUpdatedModel):
@ -26,6 +33,7 @@ class Application(UUIDModel, CreatedUpdatedModel):
name = models.TextField() name = models.TextField()
launch_url = models.URLField(null=True, blank=True) launch_url = models.URLField(null=True, blank=True)
icon_url = models.TextField(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() objects = InheritanceManager()

View File

@ -53,9 +53,9 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'reversion', 'reversion',
'rest_framework',
'passbook.core', 'passbook.core',
'passbook.admin', 'passbook.admin',
'rest_framework',
'passbook.lib', 'passbook.lib',
'passbook.ldap', 'passbook.ldap',
'passbook.oauth_client', 'passbook.oauth_client',

View File

@ -1,11 +1,16 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load utils %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% trans 'passbook' %}</title> <title>
{% block title %}
{% title %}
{% endblock %}
</title>
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'css/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.min.css' %}">
</head> </head>

View File

@ -24,9 +24,12 @@ urlpatterns = [
include(('passbook.admin.urls', 'passbook_admin'), namespace='passbook_admin')), include(('passbook.admin.urls', 'passbook_admin'), namespace='passbook_admin')),
path('source/oauth/', include(('passbook.oauth_client.urls', path('source/oauth/', include(('passbook.oauth_client.urls',
'passbook_oauth_client'), namespace='passbook_oauth_client')), 'passbook_oauth_client'), namespace='passbook_oauth_client')),
path('application/oauth', include(('passbook.oauth_provider.urls', path('application/oauth/', include(('passbook.oauth_provider.urls',
'passbook_oauth_provider'), 'passbook_oauth_provider'),
namespace='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: if settings.DEBUG:

View File

@ -75,7 +75,7 @@ class LoginView(UserPassesTestMixin, FormView):
login(request, user) login(request, user)
if cleaned_data.get('remember') is True: 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: else:
request.session.set_expiry(0) # Expires when browser is closed request.session.set_expiry(0) # Expires when browser is closed
messages.success(request, _("Successfully logged in!")) messages.success(request, _("Successfully logged in!"))
@ -98,4 +98,5 @@ class LoginView(UserPassesTestMixin, FormView):
context = { context = {
'reason': 'invalid', 'reason': 'invalid',
} }
raise NotImplementedError()
return render(request, 'login/invalid.html', context) return render(request, 'login/invalid.html', context)

View File

@ -9,3 +9,7 @@ class OverviewView(LoginRequiredMixin, TemplateView):
and is not being forwarded""" and is not being forwarded"""
template_name = 'overview/index.html' 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)

View File

@ -76,7 +76,7 @@ def pick(cont, arg, fallback=''):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def title(context, *title): def title(context, *title):
"""Return either just branding or title - branding""" """Return either just branding or title - branding"""
branding = Setting.get('branding', default='passbook') branding = CONFIG.y('passbook.branding', 'passbook')
if not title: if not title:
return branding return branding
# Include App Title in title # Include App Title in title

View File

@ -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 from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('passbook_core', '0001_initial'), ('passbook_core', '__first__'),
] ]
operations = [ operations = [

View File

@ -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.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -11,19 +11,16 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL), migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
('passbook_core', '0001_initial'), ('passbook_core', '__first__'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='OAuth2Application', name='OAuth2Provider',
fields=[ 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')), ('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', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)), ('oauth2_app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
], ],
options={ bases=('passbook_core.provider',),
'abstract': False,
},
bases=('passbook_core.application',),
), ),
] ]

View File

@ -1,12 +1,12 @@
"""Oauth2 provider product extension""" """Oauth2 provider product extension"""
from django.db import models 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""" """Associate an OAuth2 Application with a Product"""
oauth2 = models.ForeignKey(_OAuth2Application, on_delete=models.CASCADE) oauth2_app = models.ForeignKey(Application, on_delete=models.CASCADE)

View File

@ -8,5 +8,5 @@ urlpatterns = [
# Custom OAuth 2 Authorize View # Custom OAuth 2 Authorize View
# path('authorize/', oauth2.PassbookAuthorizationView.as_view(), name="oauth2-authorize"), # path('authorize/', oauth2.PassbookAuthorizationView.as_view(), name="oauth2-authorize"),
# OAuth API # OAuth API
path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')), path('', include('oauth2_provider.urls', namespace='oauth2_provider')),
] ]

View File

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

View File

@ -7,7 +7,7 @@ from passbook.lib.utils.reflection import class_to_path
from passbook.saml_idp.base import Processor from passbook.saml_idp.base import Processor
class SAMLRemote(Application): class SAMLApplication(Application):
"""Model to save information about a Remote SAML Endpoint""" """Model to save information about a Remote SAML Endpoint"""
acs_url = models.URLField() acs_url = models.URLField()
@ -20,4 +20,4 @@ class SAMLRemote(Application):
self._meta.get_field('processor_path').choices = processors self._meta.get_field('processor_path').choices = processors
def __str__(self): def __str__(self):
return "SAMLRemote %s (processor=%s)" % (self.name, self.processor_path) return "SAMLApplication %s (processor=%s)" % (self.name, self.processor_path)

View File

@ -3,7 +3,7 @@ from logging import getLogger
from passbook.lib.utils.reflection import path_to_class from passbook.lib.utils.reflection import path_to_class
from passbook.saml_idp.exceptions import CannotHandleAssertion from passbook.saml_idp.exceptions import CannotHandleAssertion
from passbook.saml_idp.models import SAMLRemote from passbook.saml_idp.models import SAMLApplication
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -16,7 +16,7 @@ def get_processor(remote):
def find_processor(request): def find_processor(request):
"""Returns the Processor instance that is willing to handle this 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) proc = get_processor(remote)
try: try:
if proc.can_handle(request): if proc.can_handle(request):

View File

@ -8,5 +8,5 @@ urlpatterns = [
url(r'^login/process/$', views.login_process, name='saml_login_process'), url(r'^login/process/$', views.login_process, name='saml_login_process'),
url(r'^logout/$', views.logout, name="saml_logout"), url(r'^logout/$', views.logout, name="saml_logout"),
url(r'^metadata/xml/$', views.descriptor, name='metadata_xml'), 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'),
] ]

View File

@ -17,22 +17,19 @@ from OpenSSL.crypto import FILETYPE_PEM
from OpenSSL.crypto import Error as CryptoError from OpenSSL.crypto import Error as CryptoError
from OpenSSL.crypto import load_certificate from OpenSSL.crypto import load_certificate
from passbook.core.models import Event, Setting, UserAcquirableRelationship # from passbook.core.models import Event, Setting, UserAcquirableRelationship
from passbook.core.utils import render_to_string from passbook.lib.utils.template import render_to_string
from passbook.core.views.common import ErrorResponseView # from passbook.core.views.common import ErrorResponseView
from passbook.core.views.settings import GenericSettingView # from passbook.core.views.settings import GenericSettingView
from passbook.mod.auth.saml.idp import exceptions, registry, xml_signing from passbook.saml_idp import exceptions, registry, xml_signing
from passbook.mod.auth.saml.idp.forms.settings import IDPSettingsForm
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
URL_VALIDATOR = URLValidator(schemes=('http', 'https')) URL_VALIDATOR = URLValidator(schemes=('http', 'https'))
def _generate_response(request, processor, remote): def _generate_response(request, processor, remote):
""" """Generate a SAML response using processor and return it in the proper Django
Generate a SAML response using processor and return it in the proper Django response."""
response.
"""
try: try:
ctx = processor.generate_response() ctx = processor.generate_response()
ctx['remote'] = remote ctx['remote'] = remote
@ -49,10 +46,8 @@ def render_xml(request, template, ctx):
@csrf_exempt @csrf_exempt
def login_begin(request): def login_begin(request):
""" """Receives a SAML 2.0 AuthnRequest from a Service Provider and
Receives a SAML 2.0 AuthnRequest from a Service Provider and stores it in the session prior to enforcing login."""
stores it in the session prior to enforcing login.
"""
if request.method == 'POST': if request.method == 'POST':
source = request.POST source = request.POST
else: else:
@ -65,13 +60,11 @@ def login_begin(request):
return HttpResponseBadRequest('the SAML request payload is missing') return HttpResponseBadRequest('the SAML request payload is missing')
request.session['RelayState'] = source.get('RelayState', '') 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): def redirect_to_sp(request, acs_url, saml_response, relay_state):
""" """Return autosubmit form"""
Return autosubmit form
"""
return render(request, 'core/autosubmit_form.html', { return render(request, 'core/autosubmit_form.html', {
'url': acs_url, 'url': acs_url,
'attrs': { 'attrs': {
@ -83,10 +76,8 @@ def redirect_to_sp(request, acs_url, saml_response, relay_state):
@login_required @login_required
def login_process(request): def login_process(request):
""" """Processor-based login continuation.
Processor-based login continuation. Presents a SAML 2.0 Assertion for POSTing back to the Service Provider."""
Presents a SAML 2.0 Assertion for POSTing back to the Service Provider.
"""
LOGGER.debug("Request: %s", request) LOGGER.debug("Request: %s", request)
proc, remote = registry.find_processor(request) proc, remote = registry.find_processor(request)
# Check if user has access # Check if user has access
@ -141,11 +132,9 @@ def login_process(request):
@csrf_exempt @csrf_exempt
def logout(request): 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, 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) auth.logout(request)
redirect_url = request.GET.get('redirect_to', '') redirect_url = request.GET.get('redirect_to', '')
@ -163,10 +152,8 @@ def logout(request):
@login_required @login_required
@csrf_exempt @csrf_exempt
def slo_logout(request): def slo_logout(request):
""" """Receives a SAML 2.0 LogoutRequest from a Service Provider,
Receives a SAML 2.0 LogoutRequest from a Service Provider, logs out the user and returns a standard logged-out page."""
logs out the user and returns a standard logged-out page.
"""
request.session['SAMLRequest'] = request.POST['SAMLRequest'] request.session['SAMLRequest'] = request.POST['SAMLRequest']
# TODO: Parse SAML LogoutRequest from POST data, similar to login_process(). # TODO: Parse SAML LogoutRequest from POST data, similar to login_process().
# TODO: Add a URL dispatch for this view. # TODO: Add a URL dispatch for this view.
@ -179,12 +166,10 @@ def slo_logout(request):
def descriptor(request): def descriptor(request):
""" """Replies with the XML Metadata IDSSODescriptor."""
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'))
entity_id = Setting.get('issuer') sso_url = request.build_absolute_uri(reverse('passbook_saml_idp:saml_login_begin'))
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'))
pubkey = xml_signing.load_certificate(strip=True) pubkey = xml_signing.load_certificate(strip=True)
ctx = { ctx = {
'entity_id': entity_id, 'entity_id': entity_id,
@ -194,25 +179,25 @@ def descriptor(request):
} }
metadata = render_to_string('saml/xml/metadata.xml', ctx) metadata = render_to_string('saml/xml/metadata.xml', ctx)
response = HttpResponse(metadata, content_type='application/xml') 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 return response
class IDPSettingsView(GenericSettingView): # class IDPSettingsView(GenericSettingView):
"""IDP Settings""" # """IDP Settings"""
form = IDPSettingsForm # form = IDPSettingsForm
template_name = 'saml/idp/settings.html' # template_name = 'saml/idp/settings.html'
def dispatch(self, request, *args, **kwargs): # def dispatch(self, request, *args, **kwargs):
self.extra_data['metadata'] = escape(descriptor(request).content.decode('utf-8')) # self.extra_data['metadata'] = escape(descriptor(request).content.decode('utf-8'))
# Show the certificate fingerprint # # Show the certificate fingerprint
sha1_fingerprint = _('<failed to parse certificate>') # sha1_fingerprint = _('<failed to parse certificate>')
try: # try:
cert = load_certificate(FILETYPE_PEM, Setting.get('certificate')) # cert = load_certificate(FILETYPE_PEM, CONFIG.y('saml_idp.certificate'))
sha1_fingerprint = cert.digest("sha1") # sha1_fingerprint = cert.digest("sha1")
except CryptoError: # except CryptoError:
pass # pass
self.extra_data['fingerprint'] = sha1_fingerprint # self.extra_data['fingerprint'] = sha1_fingerprint
return super().dispatch(request, *args, **kwargs) # return super().dispatch(request, *args, **kwargs)