From 62f7aee9e74000131a345e85731c82454f1431c8 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Fri, 3 Apr 2015 22:21:07 +0200 Subject: [PATCH 4/4] sync-metadata: load AttributeConsumingService sections of SAML 2.0 metadata files (fixes #6847) --- doc/sync-metadata_script.rst | 114 +++++++++++++++++++-- .../saml/management/commands/sync-metadata.py | 72 ++++++++++++- 2 files changed, 179 insertions(+), 7 deletions(-) diff --git a/doc/sync-metadata_script.rst b/doc/sync-metadata_script.rst index f2ab103..4a11887 100644 --- a/doc/sync-metadata_script.rst +++ b/doc/sync-metadata_script.rst @@ -15,16 +15,92 @@ descriptors. An example of such a file used in production is the global metadata file of the identity federation of French universities that can be found at http://... Use the following command:: path_to_project/authentic2$ python manage.py sync-metadata file_name [options] +Configuration of attributes +=========================== + +If a service provider has AttributeConsumingService nodes in its +SPSSODescriptor then we create an attribute declaration for each declared +attribute. If the attribute is optional, the attribute declaration is created +disabled. + +Currently it only supports the LDAP and the LDAP attribute profile of SAML, +i.e. SAML attribute names must be LDAP attributes oid, the NameFormat must be +URI, and an LDAP server must declared so that LDAP attributes can be resolved. +Authentic2 contains a databases of the more common LDAP schemas to help the +resolution of attributes OIDs. + +Example of an AttributeConsumingService node:: + + + Université Paris 1 - cours en ligne + + Cours en ligne de l'université + Paris 1 Panthéon - Sorbonne (LMS Moodle) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +If you do not want the attribute declarations to be automatically created pass +the option `--dont-load-attribute-consuming-service` to the `sync-metadata` command. + Options ======= * idp Load only identity providers of the metadata file. * sp @@ -38,21 +114,19 @@ Options Reloading a metadata file, when a provider with same entity is found, it is updated. If a provider in the metadata file does not exist it is created. If a provider exists in the system but not in the metadata file, it is removed. **For reloading, a source can only be associated with a unique metadata file. This is due to the fact that all providers of a source not found in - the metadata file are removed.** + the metadata file are removed.** :: -:: - - path_to_project/authentic2$ python manage.py sync-metadata file_name --source=french_federation + path_to_project/authentic2$ python manage.py sync-metadata file_name --source=french_federation * sp-policy To configure the SAML2 parameters of service providers imported with the script, a policy of type SPOptionsIdPPolicy must be created in the the administration interface. Either it is a global policy 'Default' or 'All' or it is a regular policy. If it is a regular policy, the policy name can be specified in parameter @@ -68,23 +142,51 @@ Options To configure the SAML2 parameters of identity providers imported with the script, a policy of type IdPOptionsSPPolicy must be created in the the administration interface. Either it is a global policy 'Default' or 'All' or it is a regular policy. If it is a regular policy, the policy name can be specified in parameter of the script with this option. The policy is then associated to all service providers created. -:: + :: - path_to_project/authentic2$ python manage.py sync-metadata file_name --idp-policy=idp_policy_name + path_to_project/authentic2$ python manage.py sync-metadata file_name --idp-policy=idp_policy_name * delete With no options, all providers are deleted. With the source option, only providers with the source name given are deleted. **This option can not be combined with options idp and sp.** * ignore-errors If loading of one EntityDescriptor fails, continue loading + +* reset-atributes + + When loading shibboleth attribute filter policies, start by removing all + existing SAML attributes for each provider, beware that it will delete any + customization of the attribute policy for each service provider. + +* dont-load-attribute-consuming-service + + Prevent loading of the attribute policy from AttributeConsumingService nodes + in the metadata file. + +* shibboleth-attribute-filter-policy + + Path to a file containing an Attribute Filter Policy for the + Shibboleth IdP, that will be used to configure SAML attributes for + each provider. The following schema is supported:: + + + + [ + + + + ]* + + + Any other kind of attribute filter policy is unsupported. diff --git a/src/authentic2/saml/management/commands/sync-metadata.py b/src/authentic2/saml/management/commands/sync-metadata.py index 6936a1e..88ba98e 100644 --- a/src/authentic2/saml/management/commands/sync-metadata.py +++ b/src/authentic2/saml/management/commands/sync-metadata.py @@ -1,25 +1,27 @@ from optparse import make_option import sys import xml.etree.ElementTree as etree import os import requests from StringIO import StringIO +import warnings from django.core.management.base import BaseCommand, CommandError from django.template.defaultfilters import slugify from django.utils.translation import gettext as _ from authentic2.compat import commit_on_success from authentic2.compat_lasso import lasso from authentic2.saml.models import * from authentic2.saml.shibboleth.afp_parser import parse_attribute_filters_file from authentic2.attribute_aggregator.core import (get_definition_from_alias, - get_full_definition, get_def_name_from_alias) + get_full_definition, get_def_name_from_alias, get_def_name_from_oid, + get_definition_from_oid) SAML2_METADATA_UI_HREF = 'urn:oasis:names:tc:SAML:metadata:ui' def md_element_name(tag_name): return '{%s}%s' % (lasso.SAML2_METADATA_HREF, tag_name) def mdui_element_name(tag_name): return '{%s}%s' % (SAML2_METADATA_UI_HREF, tag_name) @@ -27,20 +29,36 @@ def mdui_element_name(tag_name): ENTITY_DESCRIPTOR_TN = md_element_name('EntityDescriptor') ENTITIES_DESCRIPTOR_TN = md_element_name('EntitiesDescriptor') IDP_SSO_DESCRIPTOR_TN = md_element_name('IDPSSODescriptor') SP_SSO_DESCRIPTOR_TN = md_element_name('SPSSODescriptor') ORGANIZATION_DISPLAY_NAME = md_element_name('OrganizationDisplayName') ORGANIZATION_NAME = md_element_name('OrganizationName') ORGANIZATION = md_element_name('Organization') EXTENSIONS = md_element_name('Extensions') +ATTRIBUTE_CONSUMING_SERVICE = md_element_name('AttributeConsumingService') +SERVICE_NAME = md_element_name('ServiceName') +SERVICE_DESCRIPTION = md_element_name('ServiceDescription') +REQUESTED_ATTRIBUTE = md_element_name('RequestedAttribute') + UI_INFO = mdui_element_name('UIInfo') DISPLAY_NAME = mdui_element_name('DisplayName') + ENTITY_ID = 'entityID' PROTOCOL_SUPPORT_ENUMERATION = 'protocolSupportEnumeration' +IS_REQUIRED = 'isRequired' +NAME_FORMAT = 'NameFormat' +NAME = 'Name' +FRIENDLY_NAME = 'FriendlyName' + +def resolve_urn_oid(urn_oid): + if not urn_oid.startswith('urn:oid:'): + return None, None + oid = urn_oid[8:] + return get_def_name_from_oid(oid), get_definition_from_oid(oid) def build_saml_attribute_kwargs(provider, name): '''Build SAML attribute following the LDAP profile''' content_type = ContentType.objects.get_for_model(LibertyProvider) object_id = provider.pk attribute_name = name definition = get_full_definition(name) if not definition: @@ -59,16 +77,60 @@ def build_saml_attribute_kwargs(provider, name): 'friendly_name': name, } def check_support_saml2(tree): if tree is not None and lasso.SAML2_PROTOCOL_HREF in tree.get(PROTOCOL_SUPPORT_ENUMERATION): return True return False +def text_child(tree, tag, default=''): + elt = tree.find(tag) + return elt.text if not elt is None else default + +def load_acs(tree, provider, pks, verbosity): + acss = tree.iter(ATTRIBUTE_CONSUMING_SERVICE) + for acs in acss: + for ra in acs.iter(REQUESTED_ATTRIBUTE): + oid = ra.get(NAME, '') + name_format = ra.get(NAME_FORMAT, '') + friendly_name = ra.get(FRIENDLY_NAME, '') + is_required = ra.get(IS_REQUIRED, 'false') == 'true' + if name_format != lasso.SAML2_ATTRIBUTE_NAME_FORMAT_URI: + continue + def_name, defn = resolve_urn_oid(oid) + if def_name is None: + warnings.warn('attribute %s/%s unsupported on service provider %s' % ( + oid, name_format, provider.entity_id)) + continue + content_type = ContentType.objects.get_for_model(LibertyProvider) + object_id = provider.pk + kwargs = { + 'content_type': content_type, + 'object_id': object_id, + 'name_format': 'uri', + 'name': oid, + } + defaults = { + 'attribute_name': def_name.lower(), + 'friendly_name': friendly_name or def_name, + 'enabled': is_required, + } + + try: + attribute, created = SAMLAttribute.objects.get_or_create(defaults=defaults, + **kwargs) + if created and verbosity > 1: + print _('Created new attribute %(name)s for %(provider)s') % \ + {'name': oid, 'provider': provider} + pks.append(attribute.pk) + except SAMLAttribute.MultipleObjectsReturned: + pks.extend(SAMLAttribute.objects.filter(**kwargs).values_list('pk', flat=True)) + + def load_one_entity(tree, options, sp_policy=None, idp_policy=None, afp=None): '''Load or update an EntityDescriptor into the database''' verbosity = int(options['verbosity']) entity_id = tree.get(ENTITY_ID) name = None # try mdui nodes display_name = tree.find('.//%s/%s/%s' % (EXTENSIONS, UI_INFO, DISPLAY_NAME)) if display_name is not None: @@ -129,16 +191,18 @@ def load_one_entity(tree, options, sp_policy=None, idp_policy=None, afp=None): if sp: service_provider, created = LibertyServiceProvider.objects.get_or_create( liberty_provider=provider, defaults={'enabled': not options['create-disabled']}) if sp_policy: service_provider.sp_options_policy = sp_policy service_provider.save() pks = [] + if options['load_attribute_consuming_service']: + load_acs(tree, provider, pks, verbosity) if afp and provider.entity_id in afp: for name in afp[provider.entity_id]: kwargs, defaults = build_saml_attribute_kwargs(provider, name) if not kwargs: if verbosity > 1: print >>sys.stderr, _('Unable to find an LDAP definition for attribute %(name)s on %(provider)s') % \ {'name': name, 'provider': provider} continue @@ -199,16 +263,22 @@ class Command(BaseCommand): help='Tag the loaded providers with the given source string, \ existing providers with the same tag will be removed if they do not exist\ anymore in the metadata file.'), make_option('--reset-attributes', action='store_true', default=False, help='When loading shibboleth attribute filter policies, start by ' 'removing all existing SAML attributes for each provider'), + make_option('--dont-load-attribute-consuming-service', + dest='load_attribute_consuming_service', + default=True, + action='store_false', + help='Prevent loading of the attribute policy from ' + 'AttributeConsumingService nodes in the metadata file.'), make_option('--shibboleth-attribute-filter-policy', dest='attribute-filter-policy', default=None, help='''Path to a file containing an Attribute Filter Policy for the Shibboleth IdP, that will be used to configure SAML attributes for each provider. The following schema is supported: -- 1.9.1