591 lines
21 KiB
Python
591 lines
21 KiB
Python
# authentic2 - versatile identity manager
|
|
# Copyright (C) 2010-2019 Entr'ouvert
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import datetime
|
|
import logging
|
|
import os.path
|
|
import re
|
|
|
|
import requests
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
|
from django.shortcuts import render
|
|
from django.urls import reverse
|
|
from django.utils import six
|
|
from django.utils.encoding import force_text
|
|
|
|
from authentic2.compat_lasso import lasso
|
|
from authentic2.decorators import RequestCache
|
|
from authentic2.http_utils import get_url
|
|
from authentic2.idp.saml import app_settings
|
|
from authentic2.saml import models, saml2utils
|
|
from authentic2.saml.models import (
|
|
LibertyFederation,
|
|
LibertyProvider,
|
|
LibertyServiceProvider,
|
|
SPOptionsIdPPolicy,
|
|
)
|
|
|
|
from .. import nonce
|
|
|
|
AUTHENTIC_STATUS_CODE_NS = "http://authentic.entrouvert.org/status_code/"
|
|
AUTHENTIC_SAME_ID_SENTINEL = 'urn:authentic.entrouvert.org:same-as-provider-entity-id'
|
|
AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER = AUTHENTIC_STATUS_CODE_NS + "UnknownProvider"
|
|
AUTHENTIC_STATUS_CODE_MISSING_NAMEID = AUTHENTIC_STATUS_CODE_NS + "MissingNameID"
|
|
AUTHENTIC_STATUS_CODE_MISSING_SESSION_INDEX = AUTHENTIC_STATUS_CODE_NS + "MissingSessionIndex"
|
|
AUTHENTIC_STATUS_CODE_UNKNOWN_SESSION = AUTHENTIC_STATUS_CODE_NS + "UnknownSession"
|
|
AUTHENTIC_STATUS_CODE_MISSING_DESTINATION = AUTHENTIC_STATUS_CODE_NS + "MissingDestination"
|
|
AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR = AUTHENTIC_STATUS_CODE_NS + "InternalServerError"
|
|
AUTHENTIC_STATUS_CODE_UNAUTHORIZED = AUTHENTIC_STATUS_CODE_NS + "Unauthorized"
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# timeout for messages and assertions issue instant
|
|
NONCE_TIMEOUT = getattr(settings, 'SAML2_NONCE_TIMEOUT', getattr(settings, 'NONCE_TIMEOUT', 30))
|
|
# do we check id on SAML2 messages ?
|
|
CHECKS_ID = getattr(settings, 'SAML2_CHECKS_ID', True)
|
|
|
|
|
|
def get_soap_message(request):
|
|
'''Verify that POST content looks like a SOAP message and returns it'''
|
|
assert (
|
|
request.method == 'POST'
|
|
and 'CONTENT_TYPE' in request.META
|
|
and 'text/xml' in request.META['CONTENT_TYPE']
|
|
), 'not a SOAP message'
|
|
return request.body
|
|
|
|
|
|
def get_http_binding(request):
|
|
if request.method in ('GET', 'HEAD'):
|
|
return 'GET'
|
|
elif request.method == 'POST':
|
|
# disambiguate SOAP and form POST
|
|
if request.META.get('CONTENT_TYPE') in ['application/x-www-form-urlencoded', 'multipart/form-data']:
|
|
return 'POST'
|
|
else:
|
|
return 'SOAP'
|
|
|
|
|
|
# SAMLv2 methods
|
|
|
|
|
|
def get_base_path(request):
|
|
"""Get endpoints base path given metadata path"""
|
|
metadata = reverse('a2-idp-saml-metadata')
|
|
return request.build_absolute_uri(os.path.dirname(metadata))
|
|
|
|
|
|
def get_entity_id(request):
|
|
"""Return the EntityID, given metadata absolute path"""
|
|
metadata = reverse('a2-idp-saml-metadata')
|
|
return request.build_absolute_uri(metadata)
|
|
|
|
|
|
asynchronous_bindings = [lasso.SAML2_METADATA_BINDING_REDIRECT, lasso.SAML2_METADATA_BINDING_POST]
|
|
soap_bindings = [lasso.SAML2_METADATA_BINDING_SOAP]
|
|
all_bindings = asynchronous_bindings + [lasso.SAML2_METADATA_BINDING_SOAP]
|
|
|
|
|
|
def get_saml2_metadata(request, idp_map=None, sp_map=None, options={}):
|
|
metagen = saml2utils.Saml2Metadata(get_entity_id(request), url_prefix=get_base_path(request))
|
|
if idp_map:
|
|
metagen.add_idp_descriptor(idp_map, options)
|
|
if sp_map:
|
|
metagen.add_sp_descriptor(sp_map, options)
|
|
return str(metagen)
|
|
|
|
|
|
def create_saml2_server(request, idp_map=None, sp_map=None, options={}):
|
|
'''Create a lasso Server object for using with a profile'''
|
|
if app_settings.ADD_CERTIFICATE_TO_KEY_INFO:
|
|
certificate_content = options.get('key')
|
|
else:
|
|
certificate_content = None
|
|
server = lasso.Server.newFromBuffers(
|
|
get_saml2_metadata(request, idp_map=idp_map, sp_map=sp_map, options=options),
|
|
options.get('private_key'),
|
|
certificate_content=certificate_content,
|
|
)
|
|
if app_settings.SIGNATURE_METHOD:
|
|
signature_method = app_settings.SIGNATURE_METHOD
|
|
symbol_name = 'SIGNATURE_METHOD_' + signature_method.replace('-', '_').upper()
|
|
if hasattr(lasso, symbol_name):
|
|
server.signatureMethod = getattr(lasso, symbol_name)
|
|
else:
|
|
logger.warning('idp_saml: unable to set signature method %s', signature_method)
|
|
if not server:
|
|
raise Exception('Cannot create LassoServer object')
|
|
return server
|
|
|
|
|
|
def get_saml2_post_response(request):
|
|
'''Extract the SAMLRequest field from the POST'''
|
|
msg = request.POST.get(lasso.SAML2_FIELD_RESPONSE, '')
|
|
assert msg is not None, 'no message received'
|
|
logger.debug('%s: %s', lasso.SAML2_FIELD_RESPONSE, msg)
|
|
return msg
|
|
|
|
|
|
def get_saml2_post_request(request):
|
|
'''Extract the SAMLRequest field from the POST'''
|
|
return request.POST.get(lasso.SAML2_FIELD_REQUEST, '')
|
|
|
|
|
|
def get_saml2_query_request(request):
|
|
return request.META.get('QUERY_STRING', '')
|
|
|
|
|
|
def get_saml2_soap_request(request):
|
|
return get_soap_message(request)
|
|
|
|
|
|
def get_saml2_request_message_async_binding(request):
|
|
'''Return SAMLv2 message whatever the HTTP binding used'''
|
|
binding = get_http_binding(request)
|
|
if binding == 'GET':
|
|
return get_saml2_query_request(request)
|
|
elif binding == 'POST':
|
|
return get_saml2_post_request(request)
|
|
else:
|
|
raise Http404('This endpoint is only for asynchornous bindings')
|
|
|
|
|
|
def get_saml2_request_message(request, profile):
|
|
'''Return SAMLv2 message whatever the HTTP binding used'''
|
|
binding = get_http_binding(request)
|
|
if binding == 'GET':
|
|
msg = get_saml2_query_request(request)
|
|
elif binding == 'POST':
|
|
msg = get_saml2_post_request(request)
|
|
profile.msgRelayState = request.POST.get('RelayState')
|
|
elif binding == 'SOAP':
|
|
msg = get_saml2_soap_request(request)
|
|
else:
|
|
msg = None
|
|
assert msg, 'no saml2 request message found'
|
|
return msg
|
|
|
|
|
|
def return_saml2_response(request, profile, title=''):
|
|
"""Finish your SAMLv2 views with this method to return a SAML
|
|
response"""
|
|
return return_saml2(request, profile, lasso.SAML2_FIELD_RESPONSE, title)
|
|
|
|
|
|
def return_saml2_request(request, profile, title=''):
|
|
"""Finish your SAMLv2 views with this method to return a SAML
|
|
request"""
|
|
return return_saml2(request, profile, lasso.SAML2_FIELD_REQUEST, title)
|
|
|
|
|
|
def return_saml2(request, profile, field_name, title=''):
|
|
'''Helper to handle SAMLv2 bindings to emit request and responses'''
|
|
logger.debug('profile.msgBody: %r', profile.msgBody)
|
|
logger.debug('profile.msgUrl: %r', profile.msgUrl)
|
|
logger.debug('profile.msgRelayState: %r', profile.msgRelayState)
|
|
logger.debug('field_name: %s', field_name)
|
|
if profile.msgBody:
|
|
if profile.msgUrl:
|
|
return render(
|
|
request,
|
|
'saml/post_form.html',
|
|
{
|
|
'title': title,
|
|
'url': profile.msgUrl,
|
|
'fieldname': field_name,
|
|
'body': profile.msgBody,
|
|
'relay_state': profile.msgRelayState,
|
|
},
|
|
)
|
|
return HttpResponse(profile.msgBody, content_type='text/xml')
|
|
elif profile.msgUrl:
|
|
return HttpResponseRedirect(profile.msgUrl)
|
|
else:
|
|
raise TypeError('profile do not contain a response')
|
|
|
|
|
|
def check_id_and_issue_instant(request_response_or_assertion, now=None):
|
|
"""
|
|
Check that issue instant is not older than a timeout and also checks
|
|
that the id has never been seen before.
|
|
|
|
Nonce are cached for two times the relative timeout length of the issue
|
|
instant.
|
|
"""
|
|
if now is None:
|
|
now = datetime.datetime.utcnow()
|
|
try:
|
|
issue_instant = request_response_or_assertion.issueInstant
|
|
issue_instant = saml2utils.iso8601_to_datetime(issue_instant)
|
|
delta = datetime.timedelta(seconds=NONCE_TIMEOUT)
|
|
if not (now - delta <= issue_instant < now + delta):
|
|
logger.warning(
|
|
'IssueInstant %s not in the interval [%s, %s[', issue_instant, now - delta, now + delta
|
|
)
|
|
return False
|
|
except ValueError:
|
|
logger.error('Unable to parse an IssueInstant: %r', issue_instant)
|
|
return False
|
|
if CHECKS_ID:
|
|
_id = request_response_or_assertion.id
|
|
if _id is None:
|
|
logger.warning('missing ID')
|
|
return False
|
|
if not nonce.accept_nonce(_id, 'SAML', 2 * NONCE_TIMEOUT):
|
|
logger.warning("ID '%r' already used, request/response/assertion " "refused", _id)
|
|
return False
|
|
return True
|
|
|
|
|
|
def return_saml_soap_response(profile):
|
|
return HttpResponse(profile.msgBody, content_type='text/xml')
|
|
|
|
|
|
# Helper method to handle profiles endpoints
|
|
# In the future we should move away from monolithic object (LassoIdentity and
|
|
# LassoSession) holding all the datas, to manipulate them at row Level with
|
|
# LibertyFederation objects.
|
|
|
|
START_IDENTITY_DUMP = '''<Identity xmlns="http://www.entrouvert.org/namespaces/lasso/0.0" \
|
|
Version="2">
|
|
'''
|
|
MIDDLE_IDENTITY_DUMP = '''<lasso:Federation \
|
|
xmlns:lasso="http://www.entrouvert.org/namespaces/lasso/0.0" \
|
|
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" \
|
|
RemoteProviderID="{sp_id}" FederationDumpVersion="2">
|
|
<lasso:LocalNameIdentifier>
|
|
<saml:NameID Format="{format}" {qualifiers}>{content}</saml:NameID>
|
|
</lasso:LocalNameIdentifier>
|
|
</lasso:Federation>
|
|
'''
|
|
END_IDENTITY_DUMP = '''</Identity>'''
|
|
|
|
|
|
def federations_to_identity_dump(self_entity_id, federations):
|
|
l = [START_IDENTITY_DUMP] # noqa: E741
|
|
for federation in federations:
|
|
name_id_qualifier = federation.name_id_qualifier
|
|
name_id_sp_name_qualifier = federation.name_id_sp_name_qualifier
|
|
# ease migration of federations by making qualifiers relative to the
|
|
# linked idp or sp
|
|
if federation.sp:
|
|
sp_id = federation.sp.liberty_provider.entity_id
|
|
if name_id_sp_name_qualifier == AUTHENTIC_SAME_ID_SENTINEL:
|
|
name_id_sp_name_qualifier = sp_id
|
|
if name_id_qualifier == AUTHENTIC_SAME_ID_SENTINEL:
|
|
name_id_qualifier = self_entity_id
|
|
elif federation.idp:
|
|
sp_id = self_entity_id
|
|
if name_id_sp_name_qualifier == AUTHENTIC_SAME_ID_SENTINEL:
|
|
name_id_sp_name_qualifier = self_entity_id
|
|
if name_id_qualifier == AUTHENTIC_SAME_ID_SENTINEL:
|
|
name_id_qualifier = federation.idp.liberty_provider.entity_id
|
|
qualifiers = []
|
|
if federation.name_id_qualifier:
|
|
qualifiers.append('NameQualifier="%s"' % name_id_qualifier)
|
|
if federation.name_id_sp_name_qualifier:
|
|
qualifiers.append('SPNameQualifier="%s"' % name_id_sp_name_qualifier)
|
|
l.append(
|
|
MIDDLE_IDENTITY_DUMP.format(
|
|
content=federation.name_id_content,
|
|
format=federation.name_id_format,
|
|
sp_id=sp_id,
|
|
qualifiers=' '.join(qualifiers),
|
|
)
|
|
)
|
|
l.append(END_IDENTITY_DUMP)
|
|
return ''.join(l)
|
|
|
|
|
|
def load_federation(request, entity_id, login, user=None):
|
|
'''Load an identity dump from the database'''
|
|
if not user:
|
|
user = request.user
|
|
assert user is not None
|
|
|
|
identity_dump = federations_to_identity_dump(entity_id, LibertyFederation.objects.filter(user=user))
|
|
login.setIdentityFromDump(identity_dump)
|
|
|
|
|
|
def retrieve_metadata_and_create(request, provider_id, sp_or_idp):
|
|
logger.debug('trying to load %s from wkl', provider_id)
|
|
if not provider_id.startswith('http'):
|
|
logger.debug('not an http url, failing')
|
|
return None
|
|
# Try the WKL
|
|
try:
|
|
metadata = get_url(provider_id)
|
|
except Exception as e:
|
|
logging.error(
|
|
'SAML metadata autoload: failure to retrieve metadata ' 'for entity id %s: %s', provider_id, e
|
|
)
|
|
return None
|
|
logger.debug('loaded %d bytes', len(metadata))
|
|
try:
|
|
metadata = six.text_type(metadata, 'utf-8')
|
|
except UnicodeDecodeError:
|
|
logging.error('SAML metadata autoload: retrieved metadata for entity id %s is not UTF-8', provider_id)
|
|
return None
|
|
p = LibertyProvider(metadata=metadata)
|
|
try:
|
|
p.full_clean(exclude=['entity_id', 'protocol_conformance'])
|
|
except ValidationError as e:
|
|
logging.error(
|
|
'SAML metadata autoload: retrieved metadata for entity ' 'id %s are invalid, %s',
|
|
provider_id,
|
|
e.args,
|
|
)
|
|
return None
|
|
except Exception:
|
|
logging.exception(
|
|
'SAML metadata autoload: retrieved metadata ' 'validation raised an unknown exception'
|
|
)
|
|
return None
|
|
p.save()
|
|
logger.debug('%s saved', p)
|
|
s = LibertyServiceProvider(liberty_provider=p, enabled=True)
|
|
s.save()
|
|
return p
|
|
|
|
|
|
def load_provider(request, entity_id, server=None, sp_or_idp='sp', autoload=False):
|
|
"""Look up a provider in the database, and verify it handles wanted
|
|
role be it sp or idp.
|
|
|
|
Arguments:
|
|
request -- the currently handled request
|
|
entity_id -- the entity ID of the searched provider
|
|
|
|
Keyword arguments:
|
|
server -- a lasso.Server object into which to load the given provider
|
|
sp_or_idp -- kind of the provider we are looking for, can be 'sp' or
|
|
'idp', default to 'sp'
|
|
"""
|
|
try:
|
|
liberty_provider = LibertyProvider.objects.get(entity_id=entity_id)
|
|
except LibertyProvider.DoesNotExist:
|
|
autoload = getattr(settings, 'SAML_METADATA_AUTOLOAD', 'none')
|
|
if autoload and (autoload == 'sp' or autoload == 'both'):
|
|
liberty_provider = retrieve_metadata_and_create(request, entity_id, sp_or_idp)
|
|
if not liberty_provider:
|
|
return False
|
|
else:
|
|
return False
|
|
try:
|
|
service_provider = liberty_provider.service_provider
|
|
except LibertyServiceProvider.DoesNotExist:
|
|
return False
|
|
if not service_provider.enabled:
|
|
return False
|
|
if server:
|
|
server.addProviderFromBuffer(
|
|
lasso.PROVIDER_ROLE_SP, force_text(liberty_provider.metadata.encode('utf8'))
|
|
)
|
|
policy = get_sp_options_policy(liberty_provider)
|
|
if policy:
|
|
encryption_mode = 0
|
|
if policy.encrypt_assertion:
|
|
encryption_mode = lasso.ENCRYPTION_MODE_ASSERTION
|
|
if policy.encrypt_nameid:
|
|
encryption_mode = lasso.ENCRYPTION_MODE_NAMEID
|
|
server.providers[entity_id].setEncryptionMode(encryption_mode)
|
|
logger.debug('loaded provider %s', entity_id)
|
|
return liberty_provider
|
|
|
|
|
|
# Federation management
|
|
|
|
|
|
def add_federation(user, login=None, name_id=None, provider_id=None):
|
|
assert name_id or (login and login.nameIdentifier), 'missing name identifier'
|
|
name_id = name_id or login.nameIdentifier
|
|
kwargs = models.nameid2kwargs(name_id)
|
|
if provider_id:
|
|
kwargs['idp'] = LibertyProvider.objects.get(entity_id=provider_id).identity_provider
|
|
fed = LibertyFederation(user=user, **kwargs)
|
|
fed.save()
|
|
logger.debug('federation %s linked to user %s', fed.name_id_content, user)
|
|
return fed
|
|
|
|
|
|
def lookup_federation_by_name_identifier(name_id=None, profile=None):
|
|
"""Try to find a LibertyFederation object for the given NameID or
|
|
profile object."""
|
|
if not name_id:
|
|
name_id = profile.nameIdentifier
|
|
kwargs = models.nameid2kwargs(name_id)
|
|
try:
|
|
return LibertyFederation.objects.get(**kwargs)
|
|
except LibertyFederation.DoesNotExist:
|
|
return None
|
|
|
|
|
|
def lookup_federation_by_name_id_and_provider_id(name_id, provider_id):
|
|
"""Try to find a LibertyFederation object for the given NameID and
|
|
the provider id."""
|
|
kwargs = models.nameid2kwargs(name_id)
|
|
kwargs['idp'] = LibertyProvider.objects.get(entity_id=provider_id).identity_provider
|
|
try:
|
|
return LibertyFederation.objects.get(user__isnull=False, **kwargs)
|
|
except LibertyFederation.DoesNotExist:
|
|
return None
|
|
|
|
|
|
# TODO: Does it happen that a user have multiple federation with a same idp? NO
|
|
def lookup_federation_by_user(user, qualifier):
|
|
if not user or not qualifier:
|
|
return None
|
|
fed = LibertyFederation.objects.filter(user=user, name_id_qualifier=qualifier)
|
|
if fed and fed.count() > 1:
|
|
# TODO: delete all but the last record
|
|
raise Exception('Unconsistent federation record for %s' % qualifier)
|
|
if not fed:
|
|
return None
|
|
return fed[0]
|
|
|
|
|
|
class SOAPException(Exception):
|
|
pass
|
|
|
|
|
|
def soap_call(url, msg):
|
|
logger = logging.getLogger(__name__)
|
|
try:
|
|
logger.debug('SOAP call to %r with data %r', url, msg[:10000])
|
|
response = requests.post(url, data=msg, headers={'Content-Type': 'text/xml'})
|
|
response.raise_for_status()
|
|
except requests.RequestException as e:
|
|
logging.error('SOAP call to %r error %s with data %r', url, e, msg[:10000])
|
|
raise SOAPException(url, e)
|
|
logger.debug('SOAP call response %r', response.content[:10000])
|
|
return response.content
|
|
|
|
|
|
def send_soap_request(request, profile):
|
|
'''Send the SOAP request hold by the profile'''
|
|
if not profile.msgUrl or not profile.msgBody:
|
|
raise SOAPException('Missing body or url')
|
|
return soap_call(profile.msgUrl, profile.msgBody)
|
|
|
|
|
|
def set_saml2_response_responder_status_code(response, code, msg=None):
|
|
response.status = lasso.Samlp2Status()
|
|
if msg:
|
|
response.status.statusMessage = msg
|
|
response.status.statusCode = lasso.Samlp2StatusCode()
|
|
response.status.statusCode.value = lasso.SAML2_STATUS_CODE_RESPONDER
|
|
response.status.statusCode.statusCode = lasso.Samlp2StatusCode()
|
|
response.status.statusCode.statusCode.value = code
|
|
|
|
|
|
__root_refererer_re = re.compile('^(https?://[^/]*/?)')
|
|
|
|
|
|
def error_page(request, message, back=None, logger=None, warning=False):
|
|
"""View that show a simple error page to the user with a back link.
|
|
|
|
back - url for the back link, if None, return to root of the referer
|
|
or the local root.
|
|
"""
|
|
if not logger:
|
|
logger = logging
|
|
if warning:
|
|
logging.warning('Showing message %r on an error page' % message)
|
|
else:
|
|
logging.error('Showing message %r on an error page' % message)
|
|
if back is None:
|
|
referer = request.META.get('HTTP_REFERER')
|
|
if referer:
|
|
root_referer = __root_refererer_re.match(referer)
|
|
if root_referer:
|
|
back = root_referer.group(1)
|
|
if back is None:
|
|
back = '/'
|
|
redirection_timeout = getattr(settings, 'REDIRECTION_TIMEOUT_AFTER_ERROR', 2000)
|
|
return render(request, 'error.html', {'msg': message, 'back': back, 'redir_timeout': redirection_timeout})
|
|
|
|
|
|
def redirect_next(request, next):
|
|
if next:
|
|
return HttpResponseRedirect(next)
|
|
else:
|
|
return None
|
|
|
|
|
|
def soap_fault(request, faultcode='soap:Client', faultstring=None):
|
|
if faultstring:
|
|
faultstring = '\n <faultstring>%s</faultstring>\n' % faultstring
|
|
content = (
|
|
'''<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<soap:Body><soap:Fault>
|
|
<faultcode>%(faultcode)s</faultcode>%(faultstring)s
|
|
</soap:Fault></soap:Body>
|
|
</soap:Envelope>'''
|
|
% locals()
|
|
)
|
|
return HttpResponse(content, content_type="text/xml")
|
|
|
|
|
|
@RequestCache
|
|
def get_sp_options_policy_all():
|
|
try:
|
|
return SPOptionsIdPPolicy.objects.get(name='All', enabled=True)
|
|
except SPOptionsIdPPolicy.DoesNotExist:
|
|
pass
|
|
|
|
|
|
@RequestCache
|
|
def get_sp_options_policy_default():
|
|
try:
|
|
return SPOptionsIdPPolicy.objects.get(name='Default', enabled=True)
|
|
except SPOptionsIdPPolicy.DoesNotExist:
|
|
pass
|
|
|
|
|
|
def get_sp_options_policy(provider):
|
|
policy = get_sp_options_policy_all()
|
|
if policy:
|
|
return policy
|
|
if provider.service_provider.enable_following_sp_options_policy:
|
|
policy = provider.service_provider.sp_options_policy
|
|
if policy and policy.enabled:
|
|
return provider.service_provider.sp_options_policy
|
|
return get_sp_options_policy_default()
|
|
|
|
|
|
def get_session_not_on_or_after(assertion):
|
|
"""Extract the minimal value for the SessionNotOnOrAfter found in the given
|
|
assertion AuthenticationStatement(s).
|
|
"""
|
|
session_not_on_or_afters = []
|
|
if hasattr(assertion, 'authnStatement'):
|
|
for authn_statement in assertion.authnStatement:
|
|
if authn_statement.sessionNotOnOrAfter:
|
|
value = authn_statement.sessionNotOnOrAfter
|
|
try:
|
|
session_not_on_or_afters.append(saml2utils.iso8601_to_datetime(value))
|
|
except ValueError:
|
|
logging.getLogger(__name__).error(
|
|
'unable to parse SessionNotOnOrAfter value %s, will '
|
|
'use default value for session length.',
|
|
value,
|
|
)
|
|
if session_not_on_or_afters:
|
|
return six.moves.reduce(min, session_not_on_or_afters)
|
|
return None
|