diff --git a/lib/galaxy/authnz/managers.py b/lib/galaxy/authnz/managers.py index f6cb4ef8b3e2..fd37c8c06cea 100644 --- a/lib/galaxy/authnz/managers.py +++ b/lib/galaxy/authnz/managers.py @@ -22,6 +22,7 @@ string_as_bool, unicodify, ) +from galaxy.util.resources import files from .custos_authnz import ( CustosAuthFactory, KEYCLOAK_BACKENDS, @@ -34,6 +35,8 @@ Strategy, ) +OIDC_BACKEND_SCHEMA = files("galaxy.authnz.xsd") / "oidc_backends_config.xsd" + log = logging.getLogger(__name__) # Note: This if for backward compatibility. Icons can be specified in oidc_backends_config.xml. @@ -105,7 +108,7 @@ def _parse_oidc_backends_config(self, config_file): self.oidc_backends_config = {} self.oidc_backends_implementation = {} try: - tree = parse_xml(config_file) + tree = parse_xml(config_file, OIDC_BACKEND_SCHEMA) root = tree.getroot() if root.tag != "OIDC": raise etree.ParseError( diff --git a/lib/galaxy/authnz/xsd/oidc_backends_config.xsd b/lib/galaxy/authnz/xsd/oidc_backends_config.xsd new file mode 100644 index 000000000000..a8ffe7a0cd5b --- /dev/null +++ b/lib/galaxy/authnz/xsd/oidc_backends_config.xsd @@ -0,0 +1,151 @@ + + + + + + + Root element for OpenID Connect (OIDC) configurations, encompassing multiple identity providers. + + + + + + + + Configuration for a specific OIDC Identity Provider (IdP). + + + + + + + Client ID obtained from the IdP at client registration. + + + + + + + Secret generated by the IdP for the client upon registration. + + + + + + + URI where the IdP will send the authentication response. + + + + + + + Determines whether the IdP should prompt the user for re-authorization and consent. + + + + + + + URL to an icon representing the IdP. + + + + + + + Additional scopes requested from the IdP. + + + + + + + Indicates whether a confirmation page is shown for new user creation. + + + + + + + Path to a CA bundle file or directory for SSL certificate verification. + + + + + + + Override the default OIDC configuration URI. + + + + + + + Restricts the list of allowed identity providers for authentication. + + + + + + + Enable logout from the IdP when logging out of the application. + + + + + + + Custom label for the IdP in the user interface. + + + + + + + URL of the IdP. + + + + + + + API URL for the IdP, if different from the main URL. + + + + + + + Indicates support for Proof Key for Code Exchange (PKCE). + + + + + + + Hint to preselect the IdP during authentication. + + + + + + + Specifies the accepted audiences for authentication tokens. + + + + + + + + Name of the Identity Provider (IdP). + + + + + + + + + diff --git a/lib/galaxy/config/sample/oidc_backends_config.xml.sample b/lib/galaxy/config/sample/oidc_backends_config.xml.sample index 5b2c558e75b6..3683c98bdfde 100644 --- a/lib/galaxy/config/sample/oidc_backends_config.xml.sample +++ b/lib/galaxy/config/sample/oidc_backends_config.xml.sample @@ -150,6 +150,9 @@ Please mind `http` and `https`. + + diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index 541921b574d1..69c14889a387 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -294,13 +294,22 @@ def unique_id(KEY_SIZE=128): return md5(random_bits).hexdigest() -def parse_xml(fname: StrPath, strip_whitespace=True, remove_comments=True) -> ElementTree: +def parse_xml( + fname: StrPath, schemafname: Union[StrPath, None] = None, strip_whitespace=True, remove_comments=True +) -> ElementTree: """Returns a parsed xml tree""" parser = None + schema = None if remove_comments and LXML_AVAILABLE: # If using stdlib etree comments are always removed, # but lxml doesn't do this by default parser = etree.XMLParser(remove_comments=remove_comments) + + if LXML_AVAILABLE and schemafname: + with open(str(schemafname), "rb") as schema_file: + schema_root = etree.XML(schema_file.read()) + schema = etree.XMLSchema(schema_root) + try: tree = etree.parse(str(fname), parser=parser) root = tree.getroot() @@ -310,6 +319,8 @@ def parse_xml(fname: StrPath, strip_whitespace=True, remove_comments=True) -> El elem.text = elem.text.strip() if elem.tail is not None: elem.tail = elem.tail.strip() + if schema: + schema.assertValid(tree) except OSError as e: if e.errno is None and not os.path.exists(fname): # lxml doesn't set errno @@ -318,6 +329,9 @@ def parse_xml(fname: StrPath, strip_whitespace=True, remove_comments=True) -> El except etree.ParseError: log.exception("Error parsing file %s", fname) raise + except etree.DocumentInvalid as e: + log.exception(f"Validation of file %s failed with error {e}" % fname) + raise return tree