diff --git a/doc/sync-metadata_script.rst b/doc/sync-metadata_script.rst
index f2ab1037b..4a1188760 100644
--- a/doc/sync-metadata_script.rst
+++ b/doc/sync-metadata_script.rst
@@ -20,6 +20,82 @@ 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
=======
@@ -43,11 +119,9 @@ Options
**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
@@ -73,9 +147,9 @@ Options
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
@@ -88,3 +162,31 @@ Options
* 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 2a529ede3..02b30b144 100644
--- a/src/authentic2/saml/management/commands/sync-metadata.py
+++ b/src/authentic2/saml/management/commands/sync-metadata.py
@@ -4,6 +4,7 @@ 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
@@ -14,7 +15,8 @@ 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'
@@ -32,10 +34,26 @@ 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'''
@@ -64,6 +82,50 @@ def check_support_saml2(tree):
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'])
@@ -134,6 +196,8 @@ def load_one_entity(tree, options, sp_policy=None, idp_policy=None, afp=None):
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)
@@ -204,6 +268,12 @@ existing providers with the same tag will be removed if they do not exist\
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,