diff --git a/passbook/admin/templates/administration/source/list.html b/passbook/admin/templates/administration/source/list.html
index b7c00ec16..0dcd5c2da 100644
--- a/passbook/admin/templates/administration/source/list.html
+++ b/passbook/admin/templates/administration/source/list.html
@@ -36,7 +36,7 @@
{{ source.name }} |
{{ source|fieldtype }} |
- {{ source.additional_info }} |
+ {{ source.additional_info|safe }} |
{% trans 'Edit' %}
diff --git a/passbook/core/auth/view.py b/passbook/core/auth/view.py
index 710967b14..ac673298e 100644
--- a/passbook/core/auth/view.py
+++ b/passbook/core/auth/view.py
@@ -29,6 +29,7 @@ class AuthenticationView(UserPassesTestMixin, View):
SESSION_PENDING_FACTORS = 'passbook_pending_factors'
SESSION_PENDING_USER = 'passbook_pending_user'
SESSION_USER_BACKEND = 'passbook_user_backend'
+ SESSION_IS_SSO_LOGIN = 'passbook_sso_login'
pending_user = None
pending_factors = []
@@ -79,6 +80,10 @@ class AuthenticationView(UserPassesTestMixin, View):
if AuthenticationView.SESSION_FACTOR not in request.session:
# Case when no factors apply to user, return error denied
if not self.pending_factors:
+ # Case when user logged in from SSO provider and no more factors apply
+ if AuthenticationView.SESSION_IS_SSO_LOGIN in request.session:
+ LOGGER.debug("User authenticated with SSO, logging in...")
+ return self._user_passed()
return self.user_invalid()
factor_uuid, factor_class = self.pending_factors[0]
else:
diff --git a/passbook/core/forms/factors.py b/passbook/core/forms/factors.py
index 3a5b14b4d..b304eb966 100644
--- a/passbook/core/forms/factors.py
+++ b/passbook/core/forms/factors.py
@@ -27,7 +27,8 @@ class PasswordFactorForm(forms.ModelForm):
'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False),
'backends': FilteredSelectMultiple(_('backends'), False,
- choices=get_authentication_backends())
+ choices=get_authentication_backends()),
+ 'password_policies': FilteredSelectMultiple(_('password policies'), False),
}
class DummyFactorForm(forms.ModelForm):
diff --git a/passbook/core/forms/policies.py b/passbook/core/forms/policies.py
index 8fc1e5a38..7a3293c58 100644
--- a/passbook/core/forms/policies.py
+++ b/passbook/core/forms/policies.py
@@ -5,7 +5,7 @@ from django.utils.translation import gettext as _
from passbook.core.models import (DebugPolicy, FieldMatcherPolicy,
GroupMembershipPolicy, PasswordPolicy,
- WebhookPolicy)
+ SSOLoginPolicy, WebhookPolicy)
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', 'timeout']
@@ -63,6 +63,19 @@ class GroupMembershipPolicyForm(forms.ModelForm):
fields = GENERAL_FIELDS + ['group', ]
widgets = {
'name': forms.TextInput(),
+ 'order': forms.NumberInput(),
+ }
+
+class SSOLoginPolicyForm(forms.ModelForm):
+ """Edit SSOLoginPolicy instances"""
+
+ class Meta:
+
+ model = SSOLoginPolicy
+ fields = GENERAL_FIELDS
+ widgets = {
+ 'name': forms.TextInput(),
+ 'order': forms.NumberInput(),
}
class PasswordPolicyForm(forms.ModelForm):
diff --git a/passbook/core/migrations/0024_ssologinpolicy.py b/passbook/core/migrations/0024_ssologinpolicy.py
new file mode 100644
index 000000000..f783d2841
--- /dev/null
+++ b/passbook/core/migrations/0024_ssologinpolicy.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.2 on 2019-04-29 21:14
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('passbook_core', '0023_remove_user_applications'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SSOLoginPolicy',
+ fields=[
+ ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
+ ],
+ options={
+ 'verbose_name': 'SSO Login Policy',
+ 'verbose_name_plural': 'SSO Login Policies',
+ },
+ bases=('passbook_core.policy',),
+ ),
+ ]
diff --git a/passbook/core/models.py b/passbook/core/models.py
index 04140a167..fb617c60e 100644
--- a/passbook/core/models.py
+++ b/passbook/core/models.py
@@ -165,9 +165,10 @@ class Source(PolicyModel):
name = models.TextField()
slug = models.SlugField()
- form = '' # ModelForm-based class ued to create/edit instance
enabled = models.BooleanField(default=True)
+ form = '' # ModelForm-based class ued to create/edit instance
+
objects = InheritanceManager()
@property
@@ -409,6 +410,21 @@ class GroupMembershipPolicy(Policy):
verbose_name = _('Group Membership Policy')
verbose_name_plural = _('Group Membership Policies')
+class SSOLoginPolicy(Policy):
+ """Policy that applies to users that have authenticated themselves through SSO"""
+
+ form = 'passbook.core.forms.policies.SSOLoginPolicyForm'
+
+ def passes(self, user):
+ """Check if user instance passes this policy"""
+ from passbook.core.auth.view import AuthenticationView
+ return user.session.get(AuthenticationView.SESSION_IS_SSO_LOGIN, False), ""
+
+ class Meta:
+
+ verbose_name = _('SSO Login Policy')
+ verbose_name_plural = _('SSO Login Policies')
+
class Invitation(UUIDModel):
"""Single-use invitation link"""
diff --git a/passbook/core/policies.py b/passbook/core/policies.py
index 6e1f33a60..2af0385a5 100644
--- a/passbook/core/policies.py
+++ b/passbook/core/policies.py
@@ -74,6 +74,7 @@ class PolicyEngine:
cached_policies = []
kwargs = {
'__password__': getattr(self.__user, '__password__', None),
+ 'session': dict(getattr(self.__request, 'session', {}).items()),
}
if self.__request:
kwargs['remote_ip'], _ = get_client_ip(self.__request)
diff --git a/passbook/oauth_client/models.py b/passbook/oauth_client/models.py
index 94d651ee8..99f37db42 100644
--- a/passbook/oauth_client/models.py
+++ b/passbook/oauth_client/models.py
@@ -29,14 +29,13 @@ class OAuthSource(Source):
def get_login_button(self):
url = reverse_lazy('passbook_oauth_client:oauth-client-login',
kwargs={'source_slug': self.slug})
- # if self.provider_type == 'github':
- # return url, 'github-logo', _('GitHub')
return url, self.provider_type, self.name
@property
def additional_info(self):
- return "Callback URL: '%s'" % reverse_lazy('passbook_oauth_client:oauth-client-callback',
- kwargs={'source_slug': self.slug})
+ return "Callback URL: %s " % \
+ reverse_lazy('passbook_oauth_client:oauth-client-callback',
+ kwargs={'source_slug': self.slug})
def has_user_settings(self):
"""Entrypoint to integrate with User settings. Can either return False if no
diff --git a/passbook/oauth_client/views/core.py b/passbook/oauth_client/views/core.py
index a8a5e1bfa..93a9a6d10 100644
--- a/passbook/oauth_client/views/core.py
+++ b/passbook/oauth_client/views/core.py
@@ -4,7 +4,7 @@ from logging import getLogger
from django.conf import settings
from django.contrib import messages
-from django.contrib.auth import authenticate, login
+from django.contrib.auth import authenticate
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
@@ -12,6 +12,7 @@ from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views.generic import RedirectView, View
+from passbook.core.auth.view import AuthenticationView, _redirect_with_qs
from passbook.lib.utils.reflection import app
from passbook.oauth_client.clients import get_client
from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection
@@ -128,11 +129,6 @@ class OAuthCallback(OAuthClientMixin, View):
"Return url to redirect on login failure."
return settings.LOGIN_URL
- # pylint: disable=unused-argument
- def get_login_redirect(self, source, user, access, new=False):
- "Return url to redirect authenticated users."
- return 'passbook_core:overview'
-
def get_or_create_user(self, source, access, info):
"Create a shell auth.User."
raise NotImplementedError()
@@ -149,14 +145,22 @@ class OAuthCallback(OAuthClientMixin, View):
except KeyError:
return None
+ def handle_login(self, user, source, access):
+ """Prepare AuthenticationView, redirect users to remaining Factors"""
+ user = authenticate(source=access.source,
+ identifier=access.identifier, request=self.request)
+ self.request.session[AuthenticationView.SESSION_PENDING_USER] = user.pk
+ self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend
+ self.request.session[AuthenticationView.SESSION_IS_SSO_LOGIN] = True
+ return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
+
# pylint: disable=unused-argument
def handle_existing_user(self, source, user, access, info):
"Login user and redirect."
- login(self.request, user)
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
'source': self.source.name
}))
- return redirect(self.get_login_redirect(source, user, access))
+ return self.handle_login(user, source, access)
def handle_login_failure(self, source, reason):
"Message user and redirect on error."
@@ -176,12 +180,9 @@ class OAuthCallback(OAuthClientMixin, View):
access.user = user
access.save()
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
- if not was_authenticated:
- user = authenticate(source=access.source,
- identifier=access.identifier, request=self.request)
- login(self.request, user)
if app('passbook_audit'):
pass
+ # TODO: Create audit entry
# from passbook.audit.models import something
# something.event(user=user,)
# Event.create(
@@ -197,10 +198,13 @@ class OAuthCallback(OAuthClientMixin, View):
return redirect(reverse('passbook_oauth_client:oauth-client-user', kwargs={
'source_slug': self.source.slug
}))
+ # User was not authenticated, new user has been created
+ user = authenticate(source=access.source,
+ identifier=access.identifier, request=self.request)
messages.success(self.request, _("Successfully authenticated with %(source)s!" % {
'source': self.source.name
}))
- return redirect(self.get_login_redirect(source, user, access, True))
+ return self.handle_login(user, source, access)
class DisconnectView(LoginRequiredMixin, View):
|