Skip to content

Commit 23032cf

Browse files
authored
Merge pull request #348 from IdentityPython/jocar-read-xml
Possibilty to easily read and arbitrary XML
2 parents 24fc482 + d1ce8d3 commit 23032cf

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

‎src/pyff/builtins.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
root,
5858
safe_write,
5959
total_seconds,
60+
unicode_stream,
6061
utc_now,
6162
validate_document,
6263
with_tree,
@@ -1938,6 +1939,63 @@ def signcerts(req: Plumbing.Request, *opts):
19381939
return req.t
19391940

19401941

1942+
@pipe
1943+
def read_xml(req: Plumbing.Request, *opts):
1944+
"""
1945+
Make an XML file the working document.
1946+
1947+
:param req: The request
1948+
:param opts: Options (unused)
1949+
:return: the root element of the file
1950+
1951+
Unlike ``load`` this does not put anything in the store and does not split the file into entities, and it does
1952+
not depend on ``select``: the file is the working document, e.g. for signing a document that is not
1953+
SAML metadata. The file is parsed as it is and is not modified: xi:include elements are not expanded.
1954+
Takes a dict with a single key, ``file``, the path to the XML file.
1955+
1956+
**Examples**
1957+
1958+
.. code-block:: yaml
1959+
1960+
- read_xml:
1961+
file: /opt/metadata/mdsl.xml
1962+
- sign:
1963+
key: signer.key
1964+
cert: signer.crt
1965+
- emit application/xml
1966+
1967+
**Possible extensions**
1968+
1969+
- Expand xi:include elements, relative to the file, with an ``xinclude`` option.
1970+
- Ensure the root element, e.g. with a ``root`` option in ``{namespace}name`` form, to fail early when the file is
1971+
not the kind of document the rest of the pipeline expects.
1972+
- Make the working document work with ``finalize`` (and other pipes that assume entity metadata), to support
1973+
more file types than SAML metadata.
1974+
- Support ``mdsl`` (eIDAS MetadataServiceList) as a file type. The file type would ensure the root element and
1975+
tell ``finalize`` which attributes the document has. For an MDSL that is ``IssueDate`` and ``NextUpdate``
1976+
instead of ``validUntil`` and ``cacheDuration``, which the schema does not allow on this root element.
1977+
The pipeline could then look like this:
1978+
1979+
.. code-block:: yaml
1980+
1981+
- read_xml:
1982+
file: /opt/metadata/mdsl.xml
1983+
type: mdsl
1984+
- finalize:
1985+
validUntil: PT5M # sets NextUpdate, and IssueDate to the current time
1986+
- sign:
1987+
key: signer.key
1988+
cert: signer.crt
1989+
- emit application/xml
1990+
1991+
"""
1992+
if not isinstance(req.args, dict) or 'file' not in req.args:
1993+
raise PipeException("usage: read_xml {file: <path>}")
1994+
1995+
with open(req.args['file'], encoding='utf-8') as fd:
1996+
return root(parse_xml(unicode_stream(fd.read())))
1997+
1998+
19411999
@pipe
19422000
def finalize(req: Plumbing.Request, *opts):
19432001
r"""

‎src/pyff/test/test_pipeline.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,43 @@ def test_signing_single_entity(self):
733733
check_signature(published, self.public_keyspec, only_one_signature=True)
734734
os.unlink(self.output)
735735

736+
def test_read_xml_and_sign(self):
737+
"""Read an XML file as the working document and sign it without load/select."""
738+
from pyff.constants import NS
739+
from pyff.utils import check_signature
740+
741+
tmpdir = tempfile.mkdtemp()
742+
try:
743+
src = os.path.join(tmpdir, 'mdsl.xml')
744+
with open(src, 'w') as fd:
745+
fd.write(
746+
f'<ser:MetadataServiceList xmlns:ser="{NS["ser"]}" Version="1">'
747+
'<ser:IssuerName>Test</ser:IssuerName></ser:MetadataServiceList>'
748+
)
749+
res, _md = self.exec_pipeline(
750+
f"""
751+
- read_xml:
752+
file: {src}
753+
- sign:
754+
key: {self.private_keyspec}
755+
cert: {self.public_keyspec}
756+
"""
757+
)
758+
finally:
759+
shutil.rmtree(tmpdir)
760+
761+
t = root(res)
762+
assert t.tag == "{{{}}}MetadataServiceList".format(NS['ser'])
763+
assert t.get('Version') == '1'
764+
assert t[0].tag == "{{{}}}Signature".format(NS['ds'])
765+
check_signature(t, self.public_keyspec, only_one_signature=True)
766+
767+
def test_read_xml_rejects_bad_arguments(self):
768+
with pytest.raises(PipeException, match="usage: read_xml"):
769+
self.exec_pipeline("- read_xml:\n other: value\n")
770+
with pytest.raises(OSError):
771+
self.exec_pipeline("- read_xml:\n file: /nonexistent.xml\n")
772+
736773
def test_signing_and_validation(self):
737774
self.output = tempfile.NamedTemporaryFile('w').name
738775
_res_s, _md_s, _ctx_s = self.run_pipeline("signer.fd", self)

0 commit comments

Comments
 (0)