views: add new setting LOGIN_HINTS (fixes #30966)
You can set MELLON_LOGIN_HINTS = ['backoffice'] to get a node eo:login-hint set to "backoffice" in AuthnRequest when next_url for the login view is among /manage/, /admin/ or /manager/. Another value is 'always_backoffice' which always set the 'backoffice' login_hint.
This commit is contained in:
parent
b7712516ee
commit
b3e1b9c533
|
@ -38,6 +38,7 @@ class AppSettings(object):
|
|||
'LOGIN_URL': 'mellon_login',
|
||||
'LOGOUT_URL': 'mellon_logout',
|
||||
'ARTIFACT_RESOLVE_TIMEOUT': 10.0,
|
||||
'LOGIN_HINTS': [],
|
||||
}
|
||||
|
||||
@property
|
||||
|
|
|
@ -253,3 +253,13 @@ def get_xml_encoding(content):
|
|||
parser.XmlDeclHandler = xmlDeclHandler
|
||||
parser.Parse(content, True)
|
||||
return xml_encoding
|
||||
|
||||
|
||||
def get_local_path(request, url):
|
||||
if not url:
|
||||
return
|
||||
parsed = urlparse(url)
|
||||
path = parsed.path
|
||||
if request.META.get('SCRIPT_NAME'):
|
||||
path = path[len(request.META['SCRIPT_NAME']):]
|
||||
return path
|
||||
|
|
|
@ -4,6 +4,8 @@ import lasso
|
|||
import uuid
|
||||
from requests.exceptions import RequestException
|
||||
from xml.sax.saxutils import escape
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.views.generic import View
|
||||
|
@ -32,6 +34,9 @@ else:
|
|||
def lasso_decode(x):
|
||||
return x.decode('utf-8')
|
||||
|
||||
EO_NS = 'https://www.entrouvert.com/'
|
||||
LOGIN_HINT = '{%s}login-hint' % EO_NS
|
||||
|
||||
|
||||
class LogMixin(object):
|
||||
"""Initialize a module logger in new objects"""
|
||||
|
@ -40,22 +45,29 @@ class LogMixin(object):
|
|||
super(LogMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def check_next_url(request, next_url):
|
||||
log = logging.getLogger(__name__)
|
||||
if not next_url:
|
||||
return
|
||||
if not utils.is_nonnull(next_url):
|
||||
log.warning('next parameter ignored, as it contains null characters')
|
||||
return
|
||||
try:
|
||||
next_url.encode('ascii')
|
||||
except UnicodeDecodeError:
|
||||
log.warning('next parameter ignored, as is\'s not an ASCII string')
|
||||
return
|
||||
if not utils.same_origin(next_url, request.build_absolute_uri()):
|
||||
log.warning('next parameter ignored as it is not of the same origin')
|
||||
return
|
||||
return next_url
|
||||
|
||||
|
||||
class ProfileMixin(object):
|
||||
profile = None
|
||||
|
||||
def set_next_url(self, next_url):
|
||||
if not next_url:
|
||||
return
|
||||
if not utils.is_nonnull(next_url):
|
||||
self.log.warning('next parameter ignored, as it contains null characters')
|
||||
return
|
||||
try:
|
||||
next_url.encode('ascii')
|
||||
except UnicodeDecodeError:
|
||||
self.log.warning('next parameter ignored, as is\'s not an ASCII string')
|
||||
return
|
||||
if not utils.same_origin(next_url, self.request.build_absolute_uri()):
|
||||
self.log.warning('next parameter ignored as it is not of the same origin')
|
||||
if not check_next_url(self.request, next_url):
|
||||
return
|
||||
self.set_state('next_url', next_url)
|
||||
|
||||
|
@ -360,7 +372,7 @@ class LoginView(ProfileMixin, LogMixin, View):
|
|||
return self.request_discovery_service(
|
||||
request, is_passive=request.GET.get('passive') == '1')
|
||||
|
||||
next_url = request.GET.get(REDIRECT_FIELD_NAME)
|
||||
next_url = check_next_url(self.request, request.GET.get(REDIRECT_FIELD_NAME))
|
||||
idp = self.get_idp(request)
|
||||
if idp is None:
|
||||
return HttpResponseBadRequest('no idp found')
|
||||
|
@ -402,6 +414,7 @@ class LoginView(ProfileMixin, LogMixin, View):
|
|||
</samlp:Extensions>''' % eo_next_url
|
||||
)
|
||||
self.set_next_url(next_url)
|
||||
self.add_login_hints(idp, authn_request, request=request, next_url=next_url)
|
||||
login.buildAuthnRequestMsg()
|
||||
except lasso.Error as e:
|
||||
return HttpResponseBadRequest('error initializing the authentication request: %r' % e)
|
||||
|
@ -409,6 +422,39 @@ class LoginView(ProfileMixin, LogMixin, View):
|
|||
self.log.debug('to url %r', login.msgUrl)
|
||||
return HttpResponseRedirect(login.msgUrl)
|
||||
|
||||
def add_extension_node(self, authn_request, node):
|
||||
'''Factorize adding an XML node to the samlp:Extensions node'''
|
||||
if not authn_request.extensions:
|
||||
authn_request.extensions = lasso.Samlp2Extensions()
|
||||
assert hasattr(authn_request.extensions, 'any'), 'extension nodes need lasso > 2.5.1'
|
||||
serialized = ET.tostring(node, 'utf-8')
|
||||
# tostring return bytes in PY3, but lasso needs str
|
||||
if six.PY3:
|
||||
serialized = serialized.decode('utf-8')
|
||||
extension_content = authn_request.extensions.any or ()
|
||||
extension_content += (serialized,)
|
||||
authn_request.extensions.any = extension_content
|
||||
|
||||
def is_in_backoffice(self, request, next_url):
|
||||
path = utils.get_local_path(request, next_url)
|
||||
return path.startswith(('/admin/', '/manage/', '/manager/'))
|
||||
|
||||
def add_login_hints(self, idp, authn_request, request, next_url=None):
|
||||
login_hints = utils.get_setting(idp, 'LOGIN_HINTS', [])
|
||||
hints = []
|
||||
for login_hint in login_hints:
|
||||
if login_hint == 'backoffice':
|
||||
if self.is_in_backoffice(request, next_url):
|
||||
hints.append('backoffice')
|
||||
if login_hint == 'always_backoffice':
|
||||
hints.append('backoffice')
|
||||
|
||||
for hint in hints:
|
||||
node = ET.Element(LOGIN_HINT)
|
||||
node.text = hint
|
||||
self.add_extension_node(authn_request, node)
|
||||
|
||||
|
||||
# we need fine control of transactions to prevent double user creations
|
||||
login = transaction.non_atomic_requests(csrf_exempt(LoginView.as_view()))
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import re
|
||||
import base64
|
||||
import zlib
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import lasso
|
||||
|
||||
|
@ -261,3 +262,30 @@ def test_sso_artifact_no_loop(db, app, caplog, sp_settings, idp_metadata, idp_pr
|
|||
|
||||
# check return url is in page
|
||||
assert '"%s"' % sp_settings.LOGIN_REDIRECT_URL in response.text
|
||||
|
||||
|
||||
def test_sso_slo_pass_login_hints_always_backoffice(db, app, idp, caplog, sp_settings):
|
||||
sp_settings.MELLON_LOGIN_HINTS = ['always_backoffice']
|
||||
response = app.get(reverse('mellon_login') + '?next=/whatever/')
|
||||
url, body, relay_state = idp.process_authn_request_redirect(response['Location'])
|
||||
root = ET.fromstring(idp.request)
|
||||
login_hints = root.findall('.//{https://www.entrouvert.com/}login-hint')
|
||||
assert len(login_hints) == 1, 'missing login hint'
|
||||
assert login_hints[0].text == 'backoffice', 'login hint is not backoffice'
|
||||
|
||||
|
||||
def test_sso_slo_pass_login_hints_backoffice(db, app, idp, caplog, sp_settings):
|
||||
sp_settings.MELLON_LOGIN_HINTS = ['backoffice']
|
||||
response = app.get(reverse('mellon_login') + '?next=/whatever/')
|
||||
url, body, relay_state = idp.process_authn_request_redirect(response['Location'])
|
||||
root = ET.fromstring(idp.request)
|
||||
login_hints = root.findall('.//{https://www.entrouvert.com/}login-hint')
|
||||
assert len(login_hints) == 0
|
||||
|
||||
for next_url in ['/manage/', '/admin/', '/manager/']:
|
||||
response = app.get(reverse('mellon_login') + '?next=%s' % next_url)
|
||||
url, body, relay_state = idp.process_authn_request_redirect(response['Location'])
|
||||
root = ET.fromstring(idp.request)
|
||||
login_hints = root.findall('.//{https://www.entrouvert.com/}login-hint')
|
||||
assert len(login_hints) == 1, 'missing login hint'
|
||||
assert login_hints[0].text == 'backoffice', 'login hint is not backoffice'
|
||||
|
|
Loading…
Reference in New Issue