408 lines
18 KiB
Python
408 lines
18 KiB
Python
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_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)
|
|
|
|
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:
|
|
definition = get_definition_from_alias(name)
|
|
attribute_name = get_def_name_from_alias(name)
|
|
if not definition:
|
|
return {}, None
|
|
oid = definition['oid']
|
|
return {
|
|
'content_type': content_type,
|
|
'object_id': object_id,
|
|
'name_format': 'uri',
|
|
'name': 'urn:oid:%s' % oid,
|
|
}, {
|
|
'attribute_name': attribute_name.lower(),
|
|
'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:
|
|
name = display_name.text
|
|
# try "old" organization node
|
|
if not name:
|
|
organization = tree.find(ORGANIZATION)
|
|
if organization is not None:
|
|
organization_display_name = organization.find(ORGANIZATION_DISPLAY_NAME)
|
|
organization_name = organization.find(ORGANIZATION_NAME)
|
|
if organization_display_name is not None:
|
|
name = organization_display_name.text
|
|
elif organization_name is not None:
|
|
name = organization_name.text
|
|
if not name:
|
|
name = entity_id
|
|
idp, sp = False, False
|
|
idp = check_support_saml2(tree.find(IDP_SSO_DESCRIPTOR_TN))
|
|
sp = check_support_saml2(tree.find(SP_SSO_DESCRIPTOR_TN))
|
|
if options.get('idp'):
|
|
sp = False
|
|
if options.get('sp'):
|
|
idp = False
|
|
if options.get('delete'):
|
|
LibertyProvider.objects.filter(entity_id=entity_id).delete()
|
|
print 'Deleted', entity_id
|
|
return
|
|
if idp or sp:
|
|
# build an unique slug
|
|
baseslug = slug = slugify(name)
|
|
n = 1
|
|
while LibertyProvider.objects.filter(slug=slug).exclude(entity_id=entity_id):
|
|
n += 1
|
|
slug = '%s-%d' % (baseslug, n)
|
|
# get or create the provider
|
|
provider, created = LibertyProvider.objects.get_or_create(entity_id=entity_id,
|
|
protocol_conformance=3, defaults={'name': name, 'slug': slug})
|
|
if verbosity > 1:
|
|
if created:
|
|
what = 'Creating'
|
|
else:
|
|
what = 'Updating'
|
|
print '%(what)s %(name)s, %(id)s' % { 'what': what,
|
|
'name': name.encode('utf8'), 'id': entity_id}
|
|
provider.name = name
|
|
provider.metadata = etree.tostring(tree, encoding='utf-8').decode('utf-8').strip()
|
|
provider.protocol_conformance = 3
|
|
provider.federation_source = options['source']
|
|
provider.save()
|
|
options['count'] = options.get('count', 0) + 1
|
|
if idp:
|
|
identity_provider, created = LibertyIdentityProvider.objects.get_or_create(
|
|
liberty_provider=provider,
|
|
defaults={'enabled': not options['create-disabled']})
|
|
if idp_policy:
|
|
identity_provider.idp_options_policy = idp_policy
|
|
identity_provider.save()
|
|
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
|
|
# create object with default attribute mapping to the same name
|
|
# as the attribute if no SAMLAttribute model already exists,
|
|
# otherwise do nothing
|
|
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': name, 'provider': provider}
|
|
pks.append(attribute.pk)
|
|
except SAMLAttribute.MultipleObjectsReturned:
|
|
pks.extend(SAMLAttribute.objects.filter(**kwargs).values_list('pk', flat=True))
|
|
if options.get('reset-attributes'):
|
|
# remove attributes not matching the filters
|
|
SAMLAttribute.objects.for_generic_object(provider).exclude(pk__in=pks).delete()
|
|
|
|
class Command(BaseCommand):
|
|
'''Load SAMLv2 metadata file into the LibertyProvider, LibertyServiceProvider
|
|
and LibertyIdentityProvider files'''
|
|
can_import_django_settings = True
|
|
output_transaction = True
|
|
requires_model_validation = True
|
|
option_list = BaseCommand.option_list + (
|
|
make_option('--idp',
|
|
action='store_true',
|
|
dest='idp',
|
|
default=False,
|
|
help='Load identity providers only'),
|
|
make_option('--sp',
|
|
action='store_true',
|
|
dest='sp',
|
|
default=False,
|
|
help='Load service providers only'),
|
|
make_option('--sp-policy',
|
|
dest='sp_policy',
|
|
default=None,
|
|
help='SAML2 service provider options policy'),
|
|
make_option('--idp-policy',
|
|
dest='idp_policy',
|
|
default=None,
|
|
help='SAML2 identity provider options policy'),
|
|
make_option('--delete',
|
|
action='store_true',
|
|
dest='delete',
|
|
default=False,
|
|
help='Delete all providers defined in the metadata file (kind of uninstall)'),
|
|
make_option('--ignore-errors',
|
|
action='store_true',
|
|
dest='ignore-errors',
|
|
default=False,
|
|
help='If loading of one EntityDescriptor fails, continue loading'),
|
|
make_option('--source',
|
|
dest='source',
|
|
default=None,
|
|
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:
|
|
|
|
<AttributeFilterPolicy id="<whatever>">
|
|
<PolicyRequirementRule xsi:type="basic:AttributeRequesterString" value="<entityID>" >
|
|
[
|
|
<AttributeRule attributeID="<attribute-name>">
|
|
<PermitValueRule xsi:type="basic:ANY"/>
|
|
</AttributeRule>
|
|
]*
|
|
</AttributeFilterPolicy>
|
|
|
|
Any other kind of attribute filter policy is unsupported.
|
|
'''),
|
|
make_option('--create-disabled',
|
|
dest='create-disabled',
|
|
action='store_true',
|
|
default=False,
|
|
help='When creating a new provider, make it disabled by default.'),
|
|
)
|
|
|
|
args = '<metadata_file>'
|
|
help = 'Load the specified SAMLv2 metadata file'
|
|
|
|
@commit_on_success
|
|
def handle(self, *args, **options):
|
|
verbosity = int(options['verbosity'])
|
|
source = options['source']
|
|
if not args:
|
|
raise CommandError('No metadata file on the command line')
|
|
# Check sources
|
|
try:
|
|
if source is not None:
|
|
source.decode('ascii')
|
|
except:
|
|
raise CommandError('--source MUST be an ASCII string value')
|
|
if args[0].startswith('http://') or args[0].startswith('https://'):
|
|
response = requests.get(args[0])
|
|
if not response.ok:
|
|
raise CommandError('Unable to open url %s' % args[0])
|
|
metadata_file = StringIO(response.content)
|
|
else:
|
|
try:
|
|
metadata_file = file(args[0])
|
|
except:
|
|
raise CommandError('Unable to open file %s' % args[0])
|
|
|
|
try:
|
|
doc = etree.parse(metadata_file)
|
|
except Exception, e:
|
|
raise CommandError('XML parsing error: %s' % str(e))
|
|
if doc.getroot().tag == ENTITY_DESCRIPTOR_TN:
|
|
load_one_entity(doc.getroot(), options)
|
|
elif doc.getroot().tag == ENTITIES_DESCRIPTOR_TN:
|
|
afp = None
|
|
if 'attribute-filter-policy' in options and options['attribute-filter-policy']:
|
|
path = options['attribute-filter-policy']
|
|
if not os.path.isfile(path):
|
|
raise CommandError(
|
|
'No attribute filter policy file %s' % path)
|
|
afp = parse_attribute_filters_file(
|
|
options['attribute-filter-policy'])
|
|
sp_policy = None
|
|
if 'sp_policy' in options and options['sp_policy']:
|
|
sp_policy_name = options['sp_policy']
|
|
try:
|
|
sp_policy = SPOptionsIdPPolicy.objects.get(name=sp_policy_name)
|
|
if verbosity > 1:
|
|
print 'Service providers are set with the following SAML2 \
|
|
options policy: %s' % sp_policy
|
|
except:
|
|
if verbosity > 0:
|
|
print >>sys.stderr, _('SAML2 service provider options policy with name %s not found') % sp_policy_name
|
|
raise CommandError()
|
|
else:
|
|
if verbosity > 1:
|
|
print 'No SAML2 service provider options policy provided'
|
|
idp_policy = None
|
|
if 'idp_policy' in options and options['idp_policy']:
|
|
idp_policy_name = options['idp_policy']
|
|
try:
|
|
idp_policy = IdPOptionsSPPolicy.objects.get(name=idp_policy_name)
|
|
if verbosity > 1:
|
|
print 'Identity providers are set with the following SAML2 \
|
|
options policy: %s' % idp_policy
|
|
except:
|
|
if verbosity > 0:
|
|
print >>sys.stderr, _('SAML2 identity provider options policy with name %s not found') % idp_policy_name
|
|
raise CommandError()
|
|
else:
|
|
if verbosity > 1:
|
|
print _('No SAML2 identity provider options policy provided')
|
|
loaded = []
|
|
if doc.getroot().tag == ENTITY_DESCRIPTOR_TN:
|
|
entity_descriptors = [ doc.getroot() ]
|
|
else:
|
|
entity_descriptors = doc.getroot().findall(ENTITY_DESCRIPTOR_TN)
|
|
for entity_descriptor in entity_descriptors:
|
|
try:
|
|
load_one_entity(entity_descriptor, options,
|
|
sp_policy=sp_policy, idp_policy=idp_policy,
|
|
afp=afp)
|
|
loaded.append(entity_descriptor.get(ENTITY_ID))
|
|
except Exception, e:
|
|
if not options['ignore-errors']:
|
|
raise
|
|
if verbosity > 0:
|
|
print >>sys.stderr, _('Failed to load entity descriptor for %s') % entity_descriptor.get(ENTITY_ID)
|
|
raise CommandError()
|
|
if options['source']:
|
|
if options['delete']:
|
|
print 'Finally delete all providers for source: %s...' % source
|
|
LibertyProvider.objects.filter(federation_source=source).delete()
|
|
else:
|
|
to_delete = []
|
|
for provider in LibertyProvider.objects.filter(federation_source=source):
|
|
if provider.entity_id not in loaded:
|
|
to_delete.append(provider)
|
|
for provider in to_delete:
|
|
if verbosity > 1:
|
|
print _('Deleted obsolete provider %s') % provider.entity_id
|
|
provider.delete()
|
|
else:
|
|
raise CommandError('%s is not a SAMLv2 metadata file' % metadata_file)
|
|
if not options.get('delete'):
|
|
if verbosity > 1:
|
|
print 'Loaded', options.get('count', 0), 'providers'
|