django-mellon/mellon/adapters.py

278 lines
12 KiB
Python
Raw Normal View History

2014-04-28 14:33:04 +02:00
import logging
import uuid
2016-02-26 13:27:32 +01:00
from xml.etree import ElementTree as ET
import lasso
import requests
import requests.exceptions
2014-04-28 14:33:04 +02:00
from django.core.exceptions import PermissionDenied
from django.contrib import auth
from django.contrib.auth.models import Group
from django.utils.text import slugify
2014-04-28 14:33:04 +02:00
from . import utils, app_settings, models
from mellon.federation_utils import idp_metadata_store, url2filename, \
idp_metadata_extract_entity_id, idp_metadata_is_cached, \
idp_metadata_load, idp_settings_store, idp_settings_load
2014-04-28 14:33:04 +02:00
class UserCreationError(Exception):
pass
2014-04-28 14:33:04 +02:00
class DefaultAdapter(object):
def __init__(self, *args, **kwargs):
self.logger = logging.getLogger(__name__)
def get_idp(self, entity_id):
'''Find the first IdP definition matching entity_id'''
idp = None
if idp_metadata_is_cached(entity_id):
metadata_content = idp_metadata_load(entity_id)
entity_id = idp_metadata_extract_entity_id(metadata_content)
idp = {'METADATA': metadata_content,
'ENTITY_ID': entity_id}
else:
for extra_idp in self.get_identity_providers_setting():
if extra_idp.get('ENTITY_ID') == entity_id or \
idp_metadata_extract_entity_id(extra_idp) == entity_id:
idp = extra_idp.copy()
extra_idp_settings = idp_settings_load(entity_id)
if extra_idp_settings and idp:
idp.update(extra_idp_settings)
return idp
2016-02-26 13:27:32 +01:00
def get_identity_providers_setting(self):
for federation_data in self.get_federations():
if not isinstance(federation_data, dict) or \
'FEDERATION' not in federation_data:
continue
fed_extra_attrs = federation_data.copy()
fed_content = fed_extra_attrs.pop('FEDERATION')
fed_filepath, _ = utils.get_federation_metadata(fed_content)
try:
tree = ET.parse(fed_filepath)
root = tree.getroot()
for child in root:
provider = {}
entity_id = idp_metadata_extract_entity_id(ET.tostring(child))
if not entity_id:
continue
provider['METADATA'] = idp_metadata_store(ET.tostring(child))
provider.update({'ENTITY_ID': entity_id})
provider.update(fed_extra_attrs)
idp_settings_store(provider)
yield provider
except:
self.logger.error('Couldn\'t load federation metadata file %r',
fed_filepath)
continue
for extra_provider in app_settings.IDENTITY_PROVIDERS:
yield extra_provider
def get_federations(self):
for federation in getattr(app_settings, 'FEDERATIONS', []):
yield federation
2016-02-26 13:27:32 +01:00
def get_idps(self):
2016-02-26 13:27:32 +01:00
for i, idp in enumerate(self.get_identity_providers_setting()):
entity_id = idp.get('ENTITY_ID')
2016-02-26 13:27:32 +01:00
if 'METADATA_URL' in idp and 'METADATA' not in idp:
verify_ssl_certificate = utils.get_setting(
idp, 'VERIFY_SSL_CERTIFICATE')
try:
response = requests.get(idp['METADATA_URL'], verify=verify_ssl_certificate)
response.raise_for_status()
except requests.exceptions.RequestException as e:
2016-02-26 13:27:32 +01:00
self.logger.error(
u'retrieval of metadata URL %r failed with error %s for %d-th idp',
idp['METADATA_URL'], e, i)
continue
md_content = response.content
if not entity_id:
entity_id = idp_metadata_extract_entity_id(md_content)
idp['METADATA'] = idp_metadata_store(md_content)
elif not idp.get('METADATA'):
2016-02-26 13:27:32 +01:00
self.logger.error(u'missing METADATA or METADATA_URL in %d-th idp', i)
continue
# load federation-specific configuration
extra_idp_settings = idp_settings_load(entity_id)
if extra_idp_settings:
idp.update(idp_settings_load(entity_id))
2016-02-26 13:27:32 +01:00
yield idp
2014-04-28 14:33:04 +02:00
def authorize(self, idp, saml_attributes):
if not idp:
return False
required_classref = utils.get_setting(idp, 'AUTHN_CLASSREF')
2014-04-28 14:33:04 +02:00
if required_classref:
given_classref = saml_attributes['authn_context_class_ref']
if given_classref is None or \
given_classref not in required_classref:
raise PermissionDenied
return True
def format_username(self, idp, saml_attributes):
realm = utils.get_setting(idp, 'REALM')
username_template = utils.get_setting(idp, 'USERNAME_TEMPLATE')
2014-04-28 14:33:04 +02:00
try:
username = unicode(username_template).format(
realm=realm, attributes=saml_attributes, idp=idp)[:30]
2014-04-28 14:33:04 +02:00
except ValueError:
self.logger.error(u'invalid username template %r', username_template)
except (AttributeError, KeyError, IndexError) as e:
2016-02-12 17:22:48 +01:00
self.logger.error(
u'invalid reference in username template %r: %s', username_template, e)
except Exception as e:
self.logger.exception(u'unknown error when formatting username')
2014-04-28 14:33:04 +02:00
else:
return username
def create_user(self, user_class):
return user_class.objects.create(username=uuid.uuid4().hex[:30])
def finish_create_user(self, idp, saml_attributes, user):
username = self.format_username(idp, saml_attributes)
if not username:
self.logger.warning('could not build a username, login refused')
raise UserCreationError
user.username = username
user.save()
2014-04-28 14:33:04 +02:00
def lookup_user(self, idp, saml_attributes):
User = auth.get_user_model()
transient_federation_attribute = utils.get_setting(idp, 'TRANSIENT_FEDERATION_ATTRIBUTE')
if saml_attributes['name_id_format'] == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
if (transient_federation_attribute
and saml_attributes.get(transient_federation_attribute)):
name_id = saml_attributes[transient_federation_attribute]
if not isinstance(name_id, basestring):
if len(name_id) == 1:
name_id = name_id[0]
else:
self.logger.warning('more than one value for attribute %r, cannot federate',
transient_federation_attribute)
return None
else:
return None
else:
name_id = saml_attributes['name_id_content']
issuer = saml_attributes['issuer']
try:
return User.objects.get(saml_identifiers__name_id=name_id,
saml_identifiers__issuer=issuer)
except User.DoesNotExist:
if not utils.get_setting(idp, 'PROVISION'):
self.logger.warning('provisionning disabled, login refused')
return None
user = self.create_user(User)
saml_id, created = models.UserSAMLIdentifier.objects.get_or_create(
name_id=name_id, issuer=issuer, defaults={'user': user})
if created:
try:
self.finish_create_user(idp, saml_attributes, user)
except UserCreationError:
user.delete()
return None
self.logger.info('created new user %s with name_id %s from issuer %s',
user, name_id, issuer)
else:
user.delete()
user = saml_id.user
self.logger.info('looked up user %s with name_id %s from issuer %s',
user, name_id, issuer)
2014-04-28 14:33:04 +02:00
return user
def provision(self, user, idp, saml_attributes):
self.provision_attribute(user, idp, saml_attributes)
self.provision_superuser(user, idp, saml_attributes)
self.provision_groups(user, idp, saml_attributes)
def provision_attribute(self, user, idp, saml_attributes):
realm = utils.get_setting(idp, 'REALM')
attribute_mapping = utils.get_setting(idp, 'ATTRIBUTE_MAPPING')
2015-03-10 14:08:33 +01:00
attribute_set = False
2014-04-28 14:33:04 +02:00
for field, tpl in attribute_mapping.iteritems():
try:
value = unicode(tpl).format(realm=realm, attributes=saml_attributes, idp=idp)
2014-04-28 14:33:04 +02:00
except ValueError:
self.logger.warning(u'invalid attribute mapping template %r', tpl)
except (AttributeError, KeyError, IndexError, ValueError) as e:
2016-02-12 17:22:48 +01:00
self.logger.warning(
u'invalid reference in attribute mapping template %r: %s', tpl, e)
2014-04-28 14:33:04 +02:00
else:
model_field = user._meta.get_field(field)
if hasattr(model_field, 'max_length'):
value = value[:model_field.max_length]
if getattr(user, field) != value:
old_value = getattr(user, field)
setattr(user, field, value)
attribute_set = True
self.logger.info(u'set field %s of user %s to value %r (old value %r)', field,
user, value, old_value)
2015-03-10 14:08:33 +01:00
if attribute_set:
user.save()
2014-04-28 14:33:04 +02:00
def provision_superuser(self, user, idp, saml_attributes):
superuser_mapping = utils.get_setting(idp, 'SUPERUSER_MAPPING')
2014-04-28 14:33:04 +02:00
if not superuser_mapping:
return
attribute_set = False
2014-04-28 14:33:04 +02:00
for key, values in superuser_mapping.iteritems():
if key in saml_attributes:
if not isinstance(values, (tuple, list)):
values = [values]
values = set(values)
attribute_values = saml_attributes[key]
if not isinstance(attribute_values, (tuple, list)):
attribute_values = [attribute_values]
attribute_values = set(attribute_values)
if attribute_values & values:
if not (user.is_staff and user.is_superuser):
user.is_staff = True
user.is_superuser = True
attribute_set = True
self.logger.info('flag is_staff and is_superuser added to user %s', user)
break
2014-04-28 14:33:04 +02:00
else:
if user.is_superuser or user.is_staff:
user.is_staff = False
2014-04-28 14:33:04 +02:00
user.is_superuser = False
self.logger.info('flag is_staff and is_superuser removed from user %s', user)
attribute_set = True
if attribute_set:
user.save()
2014-04-28 14:33:04 +02:00
def provision_groups(self, user, idp, saml_attributes):
User = user.__class__
group_attribute = utils.get_setting(idp, 'GROUP_ATTRIBUTE')
create_group = utils.get_setting(idp, 'CREATE_GROUP')
2014-04-28 14:33:04 +02:00
if group_attribute in saml_attributes:
values = saml_attributes[group_attribute]
if not isinstance(values, (list, tuple)):
values = [values]
groups = []
for value in set(values):
if create_group:
group, created = Group.objects.get_or_create(name=value)
else:
try:
group = Group.objects.get(name=value)
except Group.DoesNotExist:
continue
groups.append(group)
for group in Group.objects.filter(pk__in=[g.pk for g in groups]).exclude(user=user):
2016-02-12 17:22:48 +01:00
self.logger.info(
u'adding group %s (%s) to user %s (%s)', group, group.pk, user, user.pk)
User.groups.through.objects.get_or_create(group=group, user=user)
2016-02-12 17:22:48 +01:00
qs = User.groups.through.objects.exclude(
group__pk__in=[g.pk for g in groups]).filter(user=user)
for rel in qs:
self.logger.info(u'removing group %s (%s) from user %s (%s)', rel.group,
rel.group.pk, rel.user, rel.user.pk)
qs.delete()