Last active
June 25, 2024 11:09
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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