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)