Skip to content

Instantly share code, notes, and snippets.

@pmeulen
Last active June 25, 2024 11:09
Show Gist options
  • Save pmeulen/8969c29407f0008614a9b89806e277a5 to your computer and use it in GitHub Desktop.
Save pmeulen/8969c29407f0008614a9b89806e277a5 to your computer and use it in GitHub Desktop.
Python script to get the first EntityDescriptor from a SAML 2.0 metadata file that has a shibmd:Scope Extension with the specified value and return the EntityDescriptor as XML
#!/usr/bin/env python3
"""
Script to get the first EntityDescriptor from a SAML 2.0 metadata file that has an IDPSSODescriptor with a shibmd:Scope
Extension with the specified value and return the EntityDescriptor as XML.
This allows you to get an EntityDescriptor for a identity provider from a large metadata file by the scope in its
EntityDescriptor.
This script uses XSLT to do the matching, so it can be easily modified to match on other criteria.
Example usage:
python3 get-entity-by-scope.py https://metadata.test.surfconext.nl/idps-metadata.xml a.test.surfconext.nl
"""
import logging
import argparse
import urllib.request
import urllib.parse
# Check if lxml is installed
try:
import lxml.etree as ET
except ImportError:
logging.error('lxml is not installed.')
logging.error('Use .e.g. "apt-get install python3-lxml", "port install py39-lxml" or "pip install lxml" to '
'install lxml.')
exit(1)
# parse commandline arguments
parser = argparse.ArgumentParser(description="""This script will parse a SAML 2.0 metadata file and returns the first
entity with an IDPSSODescriptor that has a shibmd:Scope Extension with the specified value.
Returns 0 if an entity is found, 1 if an error occurred and 2 if no entity is found.
""")
parser.add_argument('metadata', help='SAML 2.0 metadata file, this can be a URL or a local file, e.g. "https://metadata.test.surfconext.nl/idps-metadata.xml"')
parser.add_argument('scope', help='The scope to search for, e.g. "example.org"')
parser.add_argument('--loglevel', help='Set the loglevel, e.g. DEBUG, INFO, WARNING, ERROR, CRITICAL', default='INFO')
parser.add_help = True
args = parser.parse_args()
logging.basicConfig(level=args.loglevel.upper(), format='%(levelname)s: %(message)s')
# Open the metadata file. The metadata file can be a path to a local file on disk or a URL
# Use the scheme to determine if it is a URL or a local file
file = None
url = urllib.parse.urlparse(args.metadata)
if url.scheme == 'http' or url.scheme == 'https':
logging.info('Opening URL: ' + args.metadata)
if url.scheme == 'http':
logging.warning('Using HTTP to download metadata. Consider using HTTPS instead.')
try:
file = urllib.request.urlopen(args.metadata)
except (urllib.error.HTTPError, urllib.error.URLError) as e:
logging.error('Error opening URL: ' + args.metadata)
logging.error(str(e))
exit(1)
else:
# assume local file
logging.info('Opening local file: ' + args.metadata)
file = open(args.metadata, 'r')
xslt_find_scope = """<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:shibmd="urn:mace:shibboleth:metadata:1.0">
<!-- The scope to search for, passed from the xslt processor -->
<xsl:param name="scope"/>
<!-- copy any selected node with attributes and children -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Override de default template for / to select any EntityDescriptor with an IDPSSODescriptor with an
Extensions with a Scope with value specified in the scope parameter -->
<xsl:template match="/">
<xsl:apply-templates select="//md:EntityDescriptor[md:IDPSSODescriptor/md:Extensions/shibmd:Scope = $scope]"/>
</xsl:template>
</xsl:stylesheet>
"""
# Parse the metadata file using xslt to get the entity by scope
xslt_xml = ET.XML(xslt_find_scope)
xslt = ET.XSLT(xslt_xml)
logging.info('Parsing metadata file: ' + args.metadata)
try:
metadata_xml = ET.parse(file)
except ET.XMLSyntaxError as e:
logging.error('Error parsing metadata file: ' + args.metadata)
logging.error('XML Syntax Error: ' + str(e))
exit(1)
logging.info('Selecting entity with scope ' + args.scope)
# even though the xslt could return multiple entities, a DOM can only have one root element, so only the first
# entity is returned
entity_xml = xslt(metadata_xml, scope=ET.XSLT.strparam(args.scope))
xml = ET.tostring(entity_xml, pretty_print=True, encoding='unicode')
if xml is None:
logging.warning('No entity found with scope ' + args.scope)
exit(2)
# Output the matching EntityDescriptor as XML
print(xml)
exit(0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment