Skip to content

Instantly share code, notes, and snippets.

@pmeulen
Last active August 2, 2023 15:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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 a shibmd:Scope Extension with the specified
value and return the EntityDescriptor as XML.
This allows you to get an EntityDescriptor from a large metadata file by the scope of the EntityDescriptor.
The script uses XSLT to do the matching, so it can be easily modified to match on other criteria.
"""
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 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')
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