"""Functions for creating XML output.""" from __future__ import annotations from typing import TYPE_CHECKING from structlog import get_logger from passbook.channels.out_saml.utils.xml_signing import ( get_signature_xml, sign_with_signxml, ) from passbook.lib.utils.template import render_to_string if TYPE_CHECKING: from passbook.channels.out_saml.models import SAMLOutlet LOGGER = get_logger() def _get_attribute_statement(params): """Inserts AttributeStatement, if we have any attributes. Modifies the params dict. PRE-REQ: params['SUBJECT'] has already been created (usually by a call to _get_subject().""" attributes = params.get("ATTRIBUTES", []) if not attributes: params["ATTRIBUTE_STATEMENT"] = "" return # Build complete AttributeStatement. params["ATTRIBUTE_STATEMENT"] = render_to_string( "saml/xml/attributes.xml", {"attributes": attributes} ) def _get_in_response_to(params): """Insert InResponseTo if we have a RequestID. Modifies the params dict.""" # NOTE: I don't like this. We're mixing templating logic here, but the # current design requires this; maybe refactor using better templates, or # just bite the bullet and use elementtree to produce the XML; see comments # in xml_templates about Canonical XML. request_id = params.get("REQUEST_ID", None) if request_id: params["IN_RESPONSE_TO"] = 'InResponseTo="%s" ' % request_id else: params["IN_RESPONSE_TO"] = "" def _get_subject(params): """Insert Subject. Modifies the params dict.""" params["SUBJECT_STATEMENT"] = render_to_string("saml/xml/subject.xml", params) def get_assertion_xml(template, parameters, signed=False): """Get XML for Assertion""" # Reset signature. params = {} params.update(parameters) params["ASSERTION_SIGNATURE"] = "" _get_in_response_to(params) _get_subject(params) # must come before _get_attribute_statement() _get_attribute_statement(params) unsigned = render_to_string(template, params) if not signed: return unsigned # Sign it. signature_xml = get_signature_xml() params["ASSERTION_SIGNATURE"] = signature_xml return render_to_string(template, params) def get_response_xml(parameters, saml_provider: SAMLOutlet, assertion_id=""): """Returns XML for response, with signatures, if signed is True.""" # Reset signatures. params = {} params.update(parameters) params["RESPONSE_SIGNATURE"] = "" _get_in_response_to(params) raw_response = render_to_string("saml/xml/response.xml", params) if not saml_provider.signing_kp: return raw_response signature_xml = get_signature_xml() params["RESPONSE_SIGNATURE"] = signature_xml signed = sign_with_signxml(raw_response, saml_provider, reference_uri=assertion_id,) return signed