From 7d0123331670f9d9d5dbcc52d155e4a1f9aa7bd2 Mon Sep 17 00:00:00 2001 From: Vlad Mencl Date: Thu, 3 Sep 2026 12:04:00 +1200 Subject: [PATCH 1/4] fix: builtins/publish: keep xs+xsi NS in cleanup Fixes #333 The lxml.etree.cleanup_namespaces function only considers namespaces used in XML Element and Attribute names, but not in attribute values. The xs namespaces is used only in values of xsi:type attributes and is thus not considered as used by LXML - and would get removed. Keep it explicitly to avoid creating invalid metadata. Co-authored-by: Johan Wassberg --- src/pyff/builtins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyff/builtins.py b/src/pyff/builtins.py index a3a69ae2..3052dddc 100644 --- a/src/pyff/builtins.py +++ b/src/pyff/builtins.py @@ -554,7 +554,10 @@ def _nop(x): # clean unused namespaces - the working document isn't always XML (eg discojson* produce JSON) if isinstance(data, (etree._Element, etree._ElementTree)): - etree.cleanup_namespaces(data) + # Keep xs: and xsi:, as these are crucial - and xs: is used only + # in attribute values, so may appear as unused. + keep_ns_prefixes = ['xs', 'xsi'] + etree.cleanup_namespaces(data, keep_ns_prefixes=keep_ns_prefixes) if not req.args.get('raw'): data = dumptree(req.t, pretty_print=req.args.get('pretty_print')) From 01d445ea0d5911c839e3ee8ee0dce2b7b7bb140c Mon Sep 17 00:00:00 2001 From: Vlad Mencl Date: Thu, 3 Sep 2026 15:49:54 +1200 Subject: [PATCH 2/4] fix: move namespace cleanup to source The namespace cleanup was being ignored for MDQ per-entity publication - it did nothing when invoked on element other than the root element. It would also be resource intensive to clean up when publishing each individual element. Clean up instead at the source - and there, the guard condition is no longer required (the working document is always XML) Co-authored-by: Johan Wassberg --- src/pyff/builtins.py | 7 ------- src/pyff/samlmd.py | 6 ++++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pyff/builtins.py b/src/pyff/builtins.py index 3052dddc..89b623fb 100644 --- a/src/pyff/builtins.py +++ b/src/pyff/builtins.py @@ -552,13 +552,6 @@ def _nop(x): out = output_file data = req.t - # clean unused namespaces - the working document isn't always XML (eg discojson* produce JSON) - if isinstance(data, (etree._Element, etree._ElementTree)): - # Keep xs: and xsi:, as these are crucial - and xs: is used only - # in attribute values, so may appear as unused. - keep_ns_prefixes = ['xs', 'xsi'] - etree.cleanup_namespaces(data, keep_ns_prefixes=keep_ns_prefixes) - if not req.args.get('raw'): data = dumptree(req.t, pretty_print=req.args.get('pretty_print')) diff --git a/src/pyff/samlmd.py b/src/pyff/samlmd.py index b23f41b4..ae00ce6b 100644 --- a/src/pyff/samlmd.py +++ b/src/pyff/samlmd.py @@ -488,6 +488,12 @@ def entitiesdescriptor( ent_insert = deepcopy(ent_insert) t.append(ent_insert) + # clean unused namespaces + # Keep xs: and xsi:, as these are crucial - and xs: is used only + # in attribute values, so may appear as unused. + keep_ns_prefixes = ['xs', 'xsi'] + etree.cleanup_namespaces(t, keep_ns_prefixes=keep_ns_prefixes) + if config.devel_write_xml_to_file: import os From 6f42ab845448dc27391e057edc6f313918f68e77 Mon Sep 17 00:00:00 2001 From: Johan Wassberg Date: Fri, 4 Sep 2026 10:37:38 +0200 Subject: [PATCH 3/4] Added test to check for default NSes --- src/pyff/test/test_pipeline.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/pyff/test/test_pipeline.py b/src/pyff/test/test_pipeline.py index 26b2a575..81d42e14 100644 --- a/src/pyff/test/test_pipeline.py +++ b/src/pyff/test/test_pipeline.py @@ -10,6 +10,7 @@ from mako.lookup import TemplateLookup from pyff import builtins +from pyff.constants import NS from pyff.exceptions import MetadataException from pyff.parse import ParserException from pyff.pipes import PipeException, Plumbing, plumbing @@ -723,6 +724,36 @@ def test_parsecopy_(self): assert "Expected exception from bad namespace in" assert md.lookup(entity) + def test_publish_keeps_xs_xsi_namespace(self): + """cleanup_namespaces() must not strip prefixes only used inside attribute values. + + Regression test for https://github.com/IdentityPython/pyFF/issues/333: the 'xs' + prefix in swamid-2.0-test.xml is declared on an EntityDescriptor but only referenced + from within an xsi:type attribute *value* (e.g. ns3:type="xs:string"), so lxml's + cleanup_namespaces() considers it unused and removes it, producing invalid metadata. + """ + entity = 'https://xenosmilus2.umdc.umu.se:8086/coc/sp.xml' + tmpfile = tempfile.NamedTemporaryFile('w').name + try: + self.exec_pipeline( + f""" +- load: + - file://{self.datadir}/metadata/swamid-2.0-test.xml +- select +- publish: {tmpfile} +""" + ) + t2 = parse_xml(tmpfile) + assert t2 is not None + entity_elt = t2.find(".//{{{}}}EntityDescriptor[@entityID='{}']".format(NS['md'], entity)) + assert entity_elt is not None + assert entity_elt.nsmap.get('xs') == NS['xs'] + finally: + try: + os.unlink(tmpfile) + except OSError: + pass + def test_discojson_sp(self): with patch.multiple("sys", exit=self.sys_exit): tmpdir = tempfile.mkdtemp() From 7a4930a1e8e2041b7969ddc4a9746ad9987d7591 Mon Sep 17 00:00:00 2001 From: Johan Wassberg Date: Fri, 4 Sep 2026 13:00:38 +0200 Subject: [PATCH 4/4] Keep all QName defined namespaces Instead of a hardcorded list of known Qname defines --- src/pyff/samlmd.py | 38 ++++- .../data/metadata/test05-xsd-prefix-idp.xml | 57 +++++++ .../metadata/test06-xs-and-xsd-declared.xml | 145 ++++++++++++++++++ src/pyff/test/test_pipeline.py | 94 ++++++++++++ 4 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 src/pyff/test/data/metadata/test05-xsd-prefix-idp.xml create mode 100644 src/pyff/test/data/metadata/test06-xs-and-xsd-declared.xml diff --git a/src/pyff/samlmd.py b/src/pyff/samlmd.py index ae00ce6b..093e8ee3 100644 --- a/src/pyff/samlmd.py +++ b/src/pyff/samlmd.py @@ -1,4 +1,5 @@ import json +import re import traceback from base64 import b64decode from copy import deepcopy @@ -438,6 +439,35 @@ def _resolve(m, l_fn): return resolved_entities +# A QName is NCName ":" NCName. Requiring a valid NCName local part rejects the +# far more common URI-shaped attribute values ("https://..." has a '/' local part). +_QNAME_RE = re.compile(r'^[A-Za-z_][\w.-]*:[A-Za-z_][\w.-]*$') + + +def qname_prefixes(t): + """Namespace prefixes referenced from inside QName-valued attributes. + + lxml.etree.cleanup_namespaces only considers prefixes used in element and + attribute *names*, so a prefix appearing solely in an attribute *value* - + xsi:type="xsd:string" being the common case - looks unused and gets removed, + leaving a dangling QName that fails schema validation. + """ + keep = set() + match = _QNAME_RE.match + for elt in t.iter(): + values = elt.values() + if not values: + continue + for value in values: + if match(value): + prefix = value.split(':', 1)[0] + # Only keep prefixes actually bound here; anything else is either + # not a QName or already-broken metadata. + if prefix not in keep and prefix in elt.nsmap: + keep.add(prefix) + return keep + + def entitiesdescriptor( entities, name, @@ -488,10 +518,10 @@ def entitiesdescriptor( ent_insert = deepcopy(ent_insert) t.append(ent_insert) - # clean unused namespaces - # Keep xs: and xsi:, as these are crucial - and xs: is used only - # in attribute values, so may appear as unused. - keep_ns_prefixes = ['xs', 'xsi'] + # Clean unused namespaces. Prefixes referenced only from QName-valued attributes + # (xsi:type="xsd:string") are invisible to lxml's usage tracking, so collect and + # preserve them explicitly - see issue #333. + keep_ns_prefixes = sorted({'xs', 'xsi'} | qname_prefixes(t)) etree.cleanup_namespaces(t, keep_ns_prefixes=keep_ns_prefixes) if config.devel_write_xml_to_file: diff --git a/src/pyff/test/data/metadata/test05-xsd-prefix-idp.xml b/src/pyff/test/data/metadata/test05-xsd-prefix-idp.xml new file mode 100644 index 00000000..20f35e9f --- /dev/null +++ b/src/pyff/test/data/metadata/test05-xsd-prefix-idp.xml @@ -0,0 +1,57 @@ + + + + + + http://id.elegnamnden.se/ec/1.0/loa3-pnr + http://id.elegnamnden.se/st/1.0/public-sector-sp + http://id.elegnamnden.se/ec/1.0/eidas-naturalperson + + + + + + + Kronofogdens e-tjänster + Swedish Enforcement Authority + Med hjälp av våra e-tjänster kan du utföra ärenden då det passar dig bäst. Om du har en personlig e-legitimation med personnummer kan du, dygnet runt, logga in och lämna uppgifter till oss. + With the help of our e-services, you can perform tasks whenever it suits you best. If you have an e-ID with personal identity number (personnummer), you can 24 hours 365 days provide information to us. + https://www.kronofogden.se/images/18.4c1b677f134cb6b828f80003907/1371144387972/logotyp-header.png + + + + + + MIIEljCCAn6gAwIBAgIUUJYcgA6FTGauUb/a7H6GsZ5SyzkwDQYJKoZIhvcNAQELBQAwSDELMAkGA1UEBhMCU0UxFzAVBgNVBAoTDkNHSSBTdmVyaWdlIEFCMSAwHgYDVQQDExdjYS5mdW5rdGlvbnN0amFuc3Rlci5zZTAeFw0yNTAzMDMxMzM0MDBaFw0zMzEyMzEyMzU5MDBaMF4xIjAgBgNVBAMTGWF1dGguZnVua3Rpb25zdGphbnN0ZXIuc2UxFzAVBgNVBAoTDkNHSSBTdmVyaWdlIEFCMQswCQYDVQQGEwJTRTESMBAGA1UECBMJU3RvY2tob2xtMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz/rj9iRa8JOJBXRpH+4tRlwb5xZdy/aot1k6+QAmWvhSuDByMV/S0eijqfuGENq4dKnZ95pI1C074WqkN94q35HbdT+5C8kxe+ydUeVINAvpQVvRGbo0o1rPVRRdbhkj/T/KmU0Xy9gKd1KxFOv69hBe35+0hB0yVzckO6c3vGnIyIgTVbMoxz8gZ98btEyx0Z/m7J2FE7Cg2xfwrxQVEiwUQr9jeMkH5axN6aCxj0Ch14IDLl8NXhzKvJD5KUH2nt5K7tRzdDAv9GnWjRy2INxqYx3QC9AoQcpE0Sc5bhjSYY9skVJCkCD9YtUNTrqYswLbXTJQ1idbmGByQOy5wwIDAQABo2IwYDAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTxC/j/FcdqYpNS/+QIEERlqocadDALBgNVHQ8EBAMCA+gwJAYDVR0RBB0wG4IZYXV0aC5mdW5rdGlvbnN0amFuc3Rlci5zZTANBgkqhkiG9w0BAQsFAAOCAgEAsWs7B6Z8uSXPTz388OBG+OLo3vBCq5FOdow8IHzBpwbah+Ip/QLlO7q85o5uPSNglna5o5nS2D3LOfUsAhoPKYpA8vOTepEWfTLPVc6ZkyARbR+kqJlWpINjlKHlzp49YjvZhHaMsVae+8BfU1F+YIK0ArN4cBEZKFp7NafYoAb99YzdmCUyEdFRtgujAiBP8SiXLYiy04BqG8oh23OUBcOTV4KHmR9+VBMtoG31768nYzqqZTfGaXWzAsJzSOdSnmhAdRbm78oP25+lM80ZfzH0b+VqEIibCg5F8O9RYZ4aD06497M1J3Mt36nehZbAUFFCWgxOS10n6RMaZxyRtzGpVsZ5EgfFSQqM0k9IqAlm9qiHUIeg4dRI0qNY7NfeaJs8ucsHRDotGUkuJpeJpLjsSMud0eOCLKu5GUg68KOc/lA6FxdxikshA5CeB2Ull/vLk/ONXLZuD0JXhFfoqnUSQfc5zHZCrPf+H9e1LmKYpg3b82jNmHef8uhudg3DV0F0Oxe5mRrr9G0Im9dbK2nOPOw8Min0y7IsmKx+wsNhc5KSKVvGVIdPUCIOOzWmFT39WbKfXeQ3wPbEfk0WMshl69DcScHE6QipVW1e10kzgCxvXx19/yvAN0Gbf/6QAOUdIyp78I4WijMWwU4cWCbghaG0agK754M7ut2Hwnw= + + + + + + + + + MIIErDCCApSgAwIBAgIUThRHrydX8KwttHiKNPlrtADt9dAwDQYJKoZIhvcNAQELBQAwSDELMAkGA1UEBhMCU0UxFzAVBgNVBAoTDkNHSSBTdmVyaWdlIEFCMSAwHgYDVQQDExdjYS5mdW5rdGlvbnN0amFuc3Rlci5zZTAeFw0yNTA0MDcwODAwMDBaFw0zMzEyMzEyMzU5MDBaMGkxLTArBgNVBAMTJGVuY3J5cHRpb24uYXV0aC5mdW5rdGlvbnN0amFuc3Rlci5zZTEXMBUGA1UEChMOQ0dJIFN2ZXJpZ2UgQUIxCzAJBgNVBAYTAlNFMRIwEAYDVQQIEwlTdG9ja2hvbG0wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDb+xAFrd+QR7/1cfiqPcLaFqRQVsnPgESQMYkOYbN1It9D18LldPYuSFL3KubWKYYbJ67nR5mS4ACDiwp0CAaVJU2hDO7mxfcblGFmEvd2UpwWIG44vpbEqHVCCuAccU62jj9bW9QL09urWRfy6J8nXtzxpgsrRVbujzj8mAFCXi/waWQu7N3cQQV4cVuBvJUy9RVgZ15gf8uty+cFhmloZYOGnGkPctkG8aC+n5g+OQx36Cs8IGbl6LRbvSlBAyiUCEeTY0erB+FIlkddyWtDVb/pok2RWxJRBm2VmKePKpB2ykcuvYOl4jn2efSlOkU8gqKF1+FH87SvO2WvaI4XAgMBAAGjbTBrMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFHftF0eMUzkskpcB/VKCZPURDDuBMAsGA1UdDwQEAwID6DAvBgNVHREEKDAmgiRlbmNyeXB0aW9uLmF1dGguZnVua3Rpb25zdGphbnN0ZXIuc2UwDQYJKoZIhvcNAQELBQADggIBAF1oFWsnrs69LTsMgcIXlBQClt/xlinWNeSytvXQhQYCgp6h1euLRLeTt4fY/jLTO4BMHppaQ5FPcVf8Se16+LJcxMoxUGeIjbjG8eVKrT1P+ks4fZVmwMevN1z0LMCAZO/N43rLBvBakv9L7wd8atCIRUEKYP8WlyMrb5hC7MD8wdGsBd6GYBGBmdkVdVwJEvmyi5MZnWsHTxJ/DH+vCbHP3kIzZ+6CuB2oGiy5ZIScCzkeMAOzRPoqx+3PETYs2uIr71ibd3AlRRbx/gO1aEze1Lt0qN5J97/l4qi34mrSJHBlbydmepZkZAA9q7lx2hQnxhRS45T6PavzVL5f6tlp4iR99Tqb9SaLOHx5l/q23TwOttRc8rZA3LZZ7kqjq+inQ8y1KHaqgsKZFuaAEzBnJslEfQvJt3/oOzLYPzfqyamBBv+yYwrTVKzZMXurlbjeP8r+ZIDbZsHNa2wJKXpBmcfzhyJ4n66M58jh+KmRPYu2KlKA4r1eiZ/vKYXGrPCsiuY/RItTlNlagyy3+yFJDQO/5MXfL9tuoGI6GkYyZJyc585U5IjGERqcCpNegLgCHSKK29HajafOz+zPeh5Sk2qGlOcG6uehRrFPg4cGDdSqY6WEHq0ZGvYjH0GjMbStgw52KCEoWiRgsqZDhoPgsSpghntXxV+brmGK1eDE + + + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + + Kronofogden + Swedish Enforcement Authority + Kronofogden + Swedish Enforcement Authority + https://www.kronofogden.se/ + + + digitalatjanster.support@kronofogden.se + + + digitalatjanster.support@kronofogden.se + + diff --git a/src/pyff/test/data/metadata/test06-xs-and-xsd-declared.xml b/src/pyff/test/data/metadata/test06-xs-and-xsd-declared.xml new file mode 100644 index 00000000..17fa8417 --- /dev/null +++ b/src/pyff/test/data/metadata/test06-xs-and-xsd-declared.xml @@ -0,0 +1,145 @@ + + + + + + http://id.elegnamnden.se/st/1.0/sigservice + http://id.elegnamnden.se/st/1.0/public-sector-sp + http://id.swedenconnect.se/contract/sc/sweden-connect + http://id.swedenconnect.se/contract/sc/eid-authorization-system + http://id.swedenconnect.se/contract/sc/prepaid-auth-2021 + http://id.elegnamnden.se/ec/1.0/loa3-pnr + http://id.swedenconnect.se/ec/sc/uncertified-loa3-pnr + http://id.elegnamnden.se/ec/1.0/eidas-naturalperson + + + + + + + Kungliga biblioteket + National Library of Sweden + Kungliga bibliotekets e-tjänst för avancerade elektroniska underskrifter. + The National Library of Sweden's e-service for advanced electronic signatures. + https://www.kb.se/images/18.526c3ba718ee55a70f732e/1713273272431/KB-logo-text-black.svg + + + + + + + MIIGszCCBGegAwIBAgITZgAAAFw0umTVZIhT9gAAAAAAXDBBBgkqhkiG9w0BAQow + NKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAQUA + ogMCASAwdzELMAkGA1UEBhMCU0UxEzARBgNVBGEMCjU1NjM0MjY2NjYxHTAbBgNV + BAsMFENlcnRpZmljYXRlIFNlcnZpY2VzMRMwEQYDVQQKDApDb21mYWN0IEFCMR8w + HQYDVQQDDBZDb21mYWN0IFNlcnZpY2VzIENBIEcxMB4XDTI0MTAwMjA2NDI0NFoX + DTI2MTAwMjA2NDI0NFowdzELMAkGA1UEBhMCU0UxEzARBgNVBGEMCjU1NjM0MjY2 + NjYxHTAbBgNVBAsMFENlcnRpZmljYXRlIFNlcnZpY2VzMRMwEQYDVQQKDApDb21m + YWN0IEFCMR8wHQYDVQQDDBZDb21mYWN0IERTUyBLQiBTaWduaW5nMIIBojANBgkq + hkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAveGOHhhLDVEIgtiVd5EofmauucDu5YwR + x2d4H4mOFfNqfaMvCkgEytVN/MjLebnZb6J/Iav7u47Mf0+ePGKvWd+QDvE83Nvp + 1qpPtLfbcF75U+TVKM/1qOe11UzfcW9Q3mqYBr9mWM1yPHYHmOhsWESf4WGd9wFC + 80vfQvsHLEW6vbwJKgzj7fGZPiW8hzQm8SJPHRFR9+j/2IgsHQPBuURlKXflp4Yi + fyCC0hxP38vlnu5VqRRMMZBrVFHwmBKzh6ah+X1aUQdFQr2pb+cEAMAwCMNmhgln + hknudlEPx2OANHrKULgQu+2zPLwHx64vs3yHn5s2+tbdiiJT85h/WJvsrsv4F76Q + oiTzHR/VXqkviysJqKqhq1+Cua6/UQDwsgAW4lQhOlR9BM/hxH9meBxY6ELwZcYP + iQT37XAo9CcjHQzuqIb2o7yby4w1WMDVrXVOdHa+nxxfvrNIzUzJ0VUEVxozk/bE + HIHWksVra/VWxDnrNWU/9iBb4jIyJGC1AgMBAAGjggFOMIIBSjAMBgNVHRMBAf8E + AjAAMA4GA1UdDwEB/wQEAwIHgDBFBgNVHSAEPjA8MDoGByqFcIF9CAMwLzAtBggr + BgEFBQcCARYhaHR0cHM6Ly93d3cuY29tZmFjdC5zZS9yZXBvc2l0b3J5MB0GA1Ud + DgQWBBQelGRvv8HtE6iJEZWoY7/cwAutMjAfBgNVHSMEGDAWgBShC3TWdGLMTBE/ + i2xnamODhnAEijBHBgNVHR8EQDA+MDygOqA4hjZodHRwOi8vcGtpLmNvbWZhY3Qu + Y29tL2NybHMvY29tZmFjdC1zZXJ2aWNlcy1jYS1nMS5jcmwwWgYIKwYBBQUHAQEE + TjBMMEoGCCsGAQUFBzAChj5odHRwOi8vcGtpLmNvbWZhY3QuY29tL2NlcnRpZmlj + YXRlcy9jb21mYWN0LXNlcnZpY2VzLWNhLWcxLmNlcjBBBgkqhkiG9w0BAQowNKAP + MA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAQUAogMC + ASADggIBADv8b6tHD1tyNx9uY21tPwZ2ugKSupI2B5GzPr6z0gsY5iHoIBRh/WbF + Qthg2GUebqYIdj6v6RqwBG+LeQhHxy6DkJMTcY8dLSC40plSx+8s3rBFsAsxqNeu + fVuFb3fW9LO6+RzWPXeFP84ORVhV+vA3Dx+mpqPhaiL7kLgR3RH0FR6OzN6IE3GX + xDROhvc3wQjUhljK3tWi3duta7CTIDPPr2HKobuzk5orKz0b565CCP3yyVe6HgRK + vmN+cxA41LzVcd/WymndFS1W4SBgOR45B8XTEhgk8PFGypfy8SwM6sO2s5sUjS4g + 5O40649K9okXNdHAovctxYU6Qq5L4EvgSlyQWVPstDglE48ltbRAzLAHUVcCveZ5 + D6mRtmexoK6u6irCEUkq3CjH59oYK5RhaPNmMd0Ypma8j51JkyjmdkKwHbE+TZ8D + TosIyIpVE7crq8uW/WPaGVE7LsAupkfsfdBnqedsZ11/MYTbuNwuim0DGANXXVNY + swHUcVthvHmpmKTzpUYK+t4h3IV6zMRAfLbSFW89UbyGWdg0vqE6+3b7neyUoxyy + DhwpvVydDi1ws4rA8ecmGhktjPybZxOLyU4p8bwZ9WvLW3jiWDNNhPZ27JNpfJPa + p/or5jUu3rw/NaDrZ5rHhZQpipllkcbHT8xadMBQw9mInh8qFGe+ + + + + + + + + + MIIGtjCCBGqgAwIBAgITZgAAAFvnJQy/mieHIQAAAAAAWzBBBgkqhkiG9w0BAQow + NKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAQUA + ogMCASAwdzELMAkGA1UEBhMCU0UxEzARBgNVBGEMCjU1NjM0MjY2NjYxHTAbBgNV + BAsMFENlcnRpZmljYXRlIFNlcnZpY2VzMRMwEQYDVQQKDApDb21mYWN0IEFCMR8w + HQYDVQQDDBZDb21mYWN0IFNlcnZpY2VzIENBIEcxMB4XDTI0MTAwMjA2NDIyNloX + DTI2MTAwMjA2NDIyNlowejELMAkGA1UEBhMCU0UxEzARBgNVBGEMCjU1NjM0MjY2 + NjYxHTAbBgNVBAsMFENlcnRpZmljYXRlIFNlcnZpY2VzMRMwEQYDVQQKDApDb21m + YWN0IEFCMSIwIAYDVQQDDBlDb21mYWN0IERTUyBLQiBFbmNyeXB0aW9uMIIBojAN + BgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEArY/Dc1ZvM73VjHgvm3pgICb5wClM + h7e8J2l/gXeZhQ/vuO7CornScy3ny2jBzi7OMp9L+OxfCk03v4+Vf0LLfy3BBPLZ + WOFw+BThEfUb7GmNH0JxrcTp+NKI4mUKKbMrEZel5Rsb9L7LAERA8gEG+6MJwssv + 3QUsvWk2a6jQdKnERDsmbAw4/BEy5F/PLTScMs6hBV0T9yvg6eCgRmYiNvmLATaM + AQXb6qeC0uKtdsfSugxfAflmfwJZUa6vdmf83CDuhyVbXsxlisJcedkPM/aWcvNo + 0q6BmpxYr4q2ZAMpzPVqEkKq1EWREAX902YE8qnVyZoDtdLsfEN0OLzdn9dpZ3Si + 6RTuc1zpAffznEj3aO5NxpHfFgCvDguy48tSNue7+cuR7y7pmd7Ev6FDoTuhst4I + 82O9kZV2rDGqeb6sh9Kljo1TWfiZ49CnCpzbndVnwhGNPj3yFMw/L2F3iLoV+pCl + xKec+47n5AVYEZoRlMGPG/FxW90C2oQ9RWu9AgMBAAGjggFOMIIBSjAMBgNVHRMB + Af8EAjAAMA4GA1UdDwEB/wQEAwIFIDBFBgNVHSAEPjA8MDoGByqFcIF9CAMwLzAt + BggrBgEFBQcCARYhaHR0cHM6Ly93d3cuY29tZmFjdC5zZS9yZXBvc2l0b3J5MB0G + A1UdDgQWBBSRMQwi0YoN3sZWU/VvXEqPHTp7ODAfBgNVHSMEGDAWgBShC3TWdGLM + TBE/i2xnamODhnAEijBHBgNVHR8EQDA+MDygOqA4hjZodHRwOi8vcGtpLmNvbWZh + Y3QuY29tL2NybHMvY29tZmFjdC1zZXJ2aWNlcy1jYS1nMS5jcmwwWgYIKwYBBQUH + AQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vcGtpLmNvbWZhY3QuY29tL2NlcnRp + ZmljYXRlcy9jb21mYWN0LXNlcnZpY2VzLWNhLWcxLmNlcjBBBgkqhkiG9w0BAQow + NKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAQUA + ogMCASADggIBAA44+BFE/8tWF/AoC4HlaxT5gwzbQiuBEvRd3TKWIIZnQPSRbr2I + bwkA8jVa3VfA8rSvOMdyuRQvKrp/EMjI6FwFgtRfKYXI7npnHTo9WUj+BfU8F3fw + 7s15bZ4TKhzo40Gqtksrf5xJ6FwgiysF/GdFS5oC6T3jx1oU4A1gYirdIn7Ahl8G + v99V0F1zm2iS9oGhH/0aOpjjF4kT2Li4iGttp5gXpCRei3szNPo91VFDgv8ZKac4 + RgYNk2EHn80Tb1OicPtkuaKw0qw5QpdmXz0nl+v+0NmdA7P9erhGJGhEv7VDsm1f + n8jn27h3g23Ab074+HS5vt4Rpkb4xaWJMvDLHSRCkwhF5RS7cQnSSHmEgItKhq78 + lRa9EuN6u0zyzuOHy9FNK6LCq+rwc09mhy0waJBpkBVPMf5CKnn39Qi+ZtJWViTW + Xck2YbOV2yVqplWHhhkRltZc0ZfNJpqNLrbhgziGOf9FPHl20YMhHqWkE5ubiFyo + MBGvVfA19aqpRC5eBXoJoiVnxo6d26jZ0k0Z77SXget6DoJMvRFoTEzTQfClp6+S + JW68TGQo63TMuMH78gZAs9oIPxynBBbb3rPzGJnlap5BvCEjbzE/d0sSmUBltrpz + sRUy5MiIGmSSNik2yYUFkqB1qbnzNMjgQinVUFs5Lmsv04IerXxJw5Dr + + + + + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + Kungliga biblioteket + National Library of Sweden + + + + + + + + 2021001710 + + Kungliga biblioteket + National Library of Sweden + Kungliga biblioteket + National Library of Sweden + https://www.kb.se + https://www.kb.se + + + Kungliga biblioteket + ITsupport@kb.se + + + Kungliga biblioteket + ITsupport@kb.se + + diff --git a/src/pyff/test/test_pipeline.py b/src/pyff/test/test_pipeline.py index 81d42e14..918322bb 100644 --- a/src/pyff/test/test_pipeline.py +++ b/src/pyff/test/test_pipeline.py @@ -754,6 +754,100 @@ def test_publish_keeps_xs_xsi_namespace(self): except OSError: pass + def test_publish_keeps_xsd_prefix_qname(self): + """The xs/xsi hardcoded keep-list is not enough: the XMLSchema namespace can be + spelled with any prefix a document author chooses. + + Regression test with real-world metadata: test05-xsd-prefix-idp.xml declares the + XMLSchema namespace as 'xsd' (not 'xs') and uses xsi:type="xsd:string". Before this + fix, cleanup_namespaces() stripped 'xmlns:xsd' (only 'xs'/'xsi' were hardcoded), + schema validation then failed on the dangling QName, and filter_invalid silently + dropped the entity from published metadata entirely. + """ + entity = ( + 'https://auth.funktionstjanster.se/id/proxy/saml2/67cacd06db3b47c951eefdfa/' + 'idp/67cacbb7ef0f342709eefdf6/c/682f0e0f8dc75a89157ed724/authn' + ) + tmpfile = tempfile.NamedTemporaryFile('w').name + try: + self.exec_pipeline( + f""" +- load: + - file://{self.datadir}/metadata/test05-xsd-prefix-idp.xml +- select +- publish: {tmpfile} +""" + ) + t2 = parse_xml(tmpfile) + assert t2 is not None + entity_elt = t2.find(".//{{{}}}EntityDescriptor[@entityID='{}']".format(NS['md'], entity)) + assert entity_elt is not None, "entity was dropped - namespace cleanup broke schema validation" + assert entity_elt.nsmap.get('xsd') == NS['xs'] + finally: + try: + os.unlink(tmpfile) + except OSError: + pass + + def test_publish_prunes_genuinely_unused_namespace(self): + """Guard against 'fixing' future namespace reports by weakening cleanup entirely. + + test06-xs-and-xsd-declared.xml declares both 'xs' and 'xsd' bound to the same + XMLSchema URI, but only 'xs' is ever referenced (via xsi:type="xs:string"). The + genuinely unused 'xsd' declaration must still be pruned. + """ + entity = 'https://app.comfact.se/pdss/metadata/d70a69a512a2418f' + tmpfile = tempfile.NamedTemporaryFile('w').name + try: + self.exec_pipeline( + f""" +- load: + - file://{self.datadir}/metadata/test06-xs-and-xsd-declared.xml +- select +- publish: {tmpfile} +""" + ) + t2 = parse_xml(tmpfile) + assert t2 is not None + entity_elt = t2.find(".//{{{}}}EntityDescriptor[@entityID='{}']".format(NS['md'], entity)) + assert entity_elt is not None + assert entity_elt.nsmap.get('xs') == NS['xs'] + assert 'xsd' not in entity_elt.nsmap + finally: + try: + os.unlink(tmpfile) + except OSError: + pass + + def test_wsfed_type_prefix_survives(self): + """A complex-type xsi:type prefix (as opposed to a datatype prefix like xs/xsd) + needs no special-casing: real WS-Fed metadata also carries elements in that + namespace (e.g. fed:TokenTypesOffered), so lxml already sees the prefix as used. + """ + entity = 'http://idp.chalmers.se/adfs/services/trust' + tmpfile = tempfile.NamedTemporaryFile('w').name + try: + self.exec_pipeline( + f""" +- load: + - file://{self.datadir}/metadata/swamid-2.0-test.xml +- select +- publish: {tmpfile} +""" + ) + t2 = parse_xml(tmpfile) + assert t2 is not None + entity_elt = t2.find(".//{{{}}}EntityDescriptor[@entityID='{}']".format(NS['md'], entity)) + assert entity_elt is not None + role_elt = entity_elt.find("{{{}}}RoleDescriptor".format(NS['md'])) + assert role_elt is not None + assert role_elt.nsmap.get('fed') == 'http://docs.oasis-open.org/wsfed/federation/200706' + finally: + try: + os.unlink(tmpfile) + except OSError: + pass + def test_discojson_sp(self): with patch.multiple("sys", exit=self.sys_exit): tmpdir = tempfile.mkdtemp()