#![cfg(feature = "xsd-validate")]
use crate::error::Error;
use crate::xml::parse::{Element, QName};
pub(crate) const SAMLP_NS: &str = "urn:oasis:names:tc:SAML:2.0:protocol";
pub(crate) const SAML_NS: &str = "urn:oasis:names:tc:SAML:2.0:assertion";
pub(crate) const DS_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AttrPresence {
Required,
Optional,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct AttrShape {
pub namespace: Option<&'static str>,
pub local: &'static str,
pub presence: AttrPresence,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ChildShape {
pub namespace: Option<&'static str>,
pub local: &'static str,
pub min_occurs: u32,
pub shape: Option<&'static ElementShape>,
}
#[derive(Debug, Clone, Copy)]
#[expect(
dead_code,
reason = "Choice variant is part of the shape vocabulary; current SAML shapes \
all decompose to Sequence/Any but Choice is retained so future shapes \
(e.g. metadata's RoleDescriptor xs:choice) can reuse the walker."
)]
pub(crate) enum ChildModel {
Sequence(&'static [ChildShape]),
Choice(&'static [ChildShape]),
Any,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ElementShape {
pub namespace: Option<&'static str>,
pub local: &'static str,
pub attrs: &'static [AttrShape],
pub children: ChildModel,
pub wildcard_namespaces: &'static [&'static str],
}
static SHAPE_ISSUER: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "Issuer",
attrs: &[
AttrShape {
namespace: None,
local: "Format",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "NameQualifier",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "SPNameQualifier",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "SPProvidedID",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_NAMEID: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "NameID",
attrs: &[
AttrShape {
namespace: None,
local: "Format",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "NameQualifier",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "SPNameQualifier",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "SPProvidedID",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_STATUS_CODE: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "StatusCode",
attrs: &[AttrShape {
namespace: None,
local: "Value",
presence: AttrPresence::Required,
}],
children: ChildModel::Sequence(&[ChildShape {
namespace: Some(SAMLP_NS),
local: "StatusCode",
min_occurs: 0,
shape: None,
}]),
wildcard_namespaces: &[],
};
static SHAPE_STATUS_MESSAGE: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "StatusMessage",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_STATUS_DETAIL: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "StatusDetail",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_STATUS: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "Status",
attrs: &[],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAMLP_NS),
local: "StatusCode",
min_occurs: 1,
shape: Some(&SHAPE_STATUS_CODE),
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "StatusMessage",
min_occurs: 0,
shape: Some(&SHAPE_STATUS_MESSAGE),
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "StatusDetail",
min_occurs: 0,
shape: Some(&SHAPE_STATUS_DETAIL),
},
]),
wildcard_namespaces: &[],
};
static SHAPE_AUDIENCE: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "Audience",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_AUDIENCE_RESTRICTION: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "AudienceRestriction",
attrs: &[],
children: ChildModel::Sequence(&[ChildShape {
namespace: Some(SAML_NS),
local: "Audience",
min_occurs: 1,
shape: Some(&SHAPE_AUDIENCE),
}]),
wildcard_namespaces: &[],
};
static SHAPE_ONE_TIME_USE: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "OneTimeUse",
attrs: &[],
children: ChildModel::Sequence(&[]),
wildcard_namespaces: &[],
};
static SHAPE_PROXY_RESTRICTION: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "ProxyRestriction",
attrs: &[AttrShape {
namespace: None,
local: "Count",
presence: AttrPresence::Optional,
}],
children: ChildModel::Sequence(&[ChildShape {
namespace: Some(SAML_NS),
local: "Audience",
min_occurs: 0,
shape: Some(&SHAPE_AUDIENCE),
}]),
wildcard_namespaces: &[],
};
static SHAPE_CONDITIONS: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "Conditions",
attrs: &[
AttrShape {
namespace: None,
local: "NotBefore",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "NotOnOrAfter",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "AudienceRestriction",
min_occurs: 0,
shape: Some(&SHAPE_AUDIENCE_RESTRICTION),
},
ChildShape {
namespace: Some(SAML_NS),
local: "OneTimeUse",
min_occurs: 0,
shape: Some(&SHAPE_ONE_TIME_USE),
},
ChildShape {
namespace: Some(SAML_NS),
local: "ProxyRestriction",
min_occurs: 0,
shape: Some(&SHAPE_PROXY_RESTRICTION),
},
]),
wildcard_namespaces: &[SAML_NS],
};
static SHAPE_SUBJECT_CONFIRMATION_DATA: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "SubjectConfirmationData",
attrs: &[
AttrShape {
namespace: None,
local: "NotBefore",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "NotOnOrAfter",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "Recipient",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "InResponseTo",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "Address",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_SUBJECT_CONFIRMATION: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "SubjectConfirmation",
attrs: &[AttrShape {
namespace: None,
local: "Method",
presence: AttrPresence::Required,
}],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "NameID",
min_occurs: 0,
shape: Some(&SHAPE_NAMEID),
},
ChildShape {
namespace: Some(SAML_NS),
local: "SubjectConfirmationData",
min_occurs: 0,
shape: Some(&SHAPE_SUBJECT_CONFIRMATION_DATA),
},
]),
wildcard_namespaces: &[],
};
static SHAPE_SUBJECT: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "Subject",
attrs: &[],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "NameID",
min_occurs: 0,
shape: Some(&SHAPE_NAMEID),
},
ChildShape {
namespace: Some(SAML_NS),
local: "SubjectConfirmation",
min_occurs: 0,
shape: Some(&SHAPE_SUBJECT_CONFIRMATION),
},
]),
wildcard_namespaces: &[SAML_NS],
};
static SHAPE_AUTHN_CONTEXT_CLASS_REF: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "AuthnContextClassRef",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_AUTHN_CONTEXT_DECL_REF: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "AuthnContextDeclRef",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_AUTHN_CONTEXT_DECL: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "AuthnContextDecl",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_AUTHENTICATING_AUTHORITY: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "AuthenticatingAuthority",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_AUTHN_CONTEXT: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "AuthnContext",
attrs: &[],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "AuthnContextClassRef",
min_occurs: 0,
shape: Some(&SHAPE_AUTHN_CONTEXT_CLASS_REF),
},
ChildShape {
namespace: Some(SAML_NS),
local: "AuthnContextDecl",
min_occurs: 0,
shape: Some(&SHAPE_AUTHN_CONTEXT_DECL),
},
ChildShape {
namespace: Some(SAML_NS),
local: "AuthnContextDeclRef",
min_occurs: 0,
shape: Some(&SHAPE_AUTHN_CONTEXT_DECL_REF),
},
ChildShape {
namespace: Some(SAML_NS),
local: "AuthenticatingAuthority",
min_occurs: 0,
shape: Some(&SHAPE_AUTHENTICATING_AUTHORITY),
},
]),
wildcard_namespaces: &[],
};
static SHAPE_SUBJECT_LOCALITY: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "SubjectLocality",
attrs: &[
AttrShape {
namespace: None,
local: "Address",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "DNSName",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Sequence(&[]),
wildcard_namespaces: &[],
};
static SHAPE_AUTHN_STATEMENT: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "AuthnStatement",
attrs: &[
AttrShape {
namespace: None,
local: "AuthnInstant",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "SessionIndex",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "SessionNotOnOrAfter",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "SubjectLocality",
min_occurs: 0,
shape: Some(&SHAPE_SUBJECT_LOCALITY),
},
ChildShape {
namespace: Some(SAML_NS),
local: "AuthnContext",
min_occurs: 1,
shape: Some(&SHAPE_AUTHN_CONTEXT),
},
]),
wildcard_namespaces: &[],
};
static SHAPE_ATTRIBUTE_VALUE: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "AttributeValue",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_ATTRIBUTE: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "Attribute",
attrs: &[
AttrShape {
namespace: None,
local: "Name",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "NameFormat",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "FriendlyName",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Sequence(&[ChildShape {
namespace: Some(SAML_NS),
local: "AttributeValue",
min_occurs: 0,
shape: Some(&SHAPE_ATTRIBUTE_VALUE),
}]),
wildcard_namespaces: &[],
};
static SHAPE_ATTRIBUTE_STATEMENT: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "AttributeStatement",
attrs: &[],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "Attribute",
min_occurs: 1,
shape: Some(&SHAPE_ATTRIBUTE),
},
ChildShape {
namespace: Some(SAML_NS),
local: "EncryptedAttribute",
min_occurs: 0,
shape: None,
},
]),
wildcard_namespaces: &[],
};
static SHAPE_ASSERTION: ElementShape = ElementShape {
namespace: Some(SAML_NS),
local: "Assertion",
attrs: &[
AttrShape {
namespace: None,
local: "ID",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "Version",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "IssueInstant",
presence: AttrPresence::Required,
},
],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "Issuer",
min_occurs: 1,
shape: Some(&SHAPE_ISSUER),
},
ChildShape {
namespace: Some(DS_NS),
local: "Signature",
min_occurs: 0,
shape: None,
},
ChildShape {
namespace: Some(SAML_NS),
local: "Subject",
min_occurs: 0,
shape: Some(&SHAPE_SUBJECT),
},
ChildShape {
namespace: Some(SAML_NS),
local: "Conditions",
min_occurs: 0,
shape: Some(&SHAPE_CONDITIONS),
},
ChildShape {
namespace: Some(SAML_NS),
local: "Advice",
min_occurs: 0,
shape: None,
},
ChildShape {
namespace: Some(SAML_NS),
local: "AuthnStatement",
min_occurs: 0,
shape: Some(&SHAPE_AUTHN_STATEMENT),
},
ChildShape {
namespace: Some(SAML_NS),
local: "AttributeStatement",
min_occurs: 0,
shape: Some(&SHAPE_ATTRIBUTE_STATEMENT),
},
]),
wildcard_namespaces: &[SAML_NS],
};
static SHAPE_EXTENSIONS: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "Extensions",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_RESPONSE: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "Response",
attrs: &[
AttrShape {
namespace: None,
local: "ID",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "Version",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "IssueInstant",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "InResponseTo",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "Destination",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "Consent",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "Issuer",
min_occurs: 0,
shape: Some(&SHAPE_ISSUER),
},
ChildShape {
namespace: Some(DS_NS),
local: "Signature",
min_occurs: 0,
shape: None,
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "Extensions",
min_occurs: 0,
shape: Some(&SHAPE_EXTENSIONS),
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "Status",
min_occurs: 1,
shape: Some(&SHAPE_STATUS),
},
ChildShape {
namespace: Some(SAML_NS),
local: "Assertion",
min_occurs: 0,
shape: Some(&SHAPE_ASSERTION),
},
ChildShape {
namespace: Some(SAML_NS),
local: "EncryptedAssertion",
min_occurs: 0,
shape: None,
},
]),
wildcard_namespaces: &[],
};
static SHAPE_NAMEID_POLICY: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "NameIDPolicy",
attrs: &[
AttrShape {
namespace: None,
local: "Format",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "SPNameQualifier",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "AllowCreate",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Sequence(&[]),
wildcard_namespaces: &[],
};
static SHAPE_REQUESTED_AUTHN_CONTEXT: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "RequestedAuthnContext",
attrs: &[AttrShape {
namespace: None,
local: "Comparison",
presence: AttrPresence::Optional,
}],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "AuthnContextClassRef",
min_occurs: 0,
shape: Some(&SHAPE_AUTHN_CONTEXT_CLASS_REF),
},
ChildShape {
namespace: Some(SAML_NS),
local: "AuthnContextDeclRef",
min_occurs: 0,
shape: Some(&SHAPE_AUTHN_CONTEXT_DECL_REF),
},
]),
wildcard_namespaces: &[],
};
static SHAPE_SCOPING: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "Scoping",
attrs: &[AttrShape {
namespace: None,
local: "ProxyCount",
presence: AttrPresence::Optional,
}],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_REQUEST_CONDITIONS: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "Conditions",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_AUTHN_REQUEST: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "AuthnRequest",
attrs: &[
AttrShape {
namespace: None,
local: "ID",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "Version",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "IssueInstant",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "Destination",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "Consent",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "ForceAuthn",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "IsPassive",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "ProtocolBinding",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "AssertionConsumerServiceIndex",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "AssertionConsumerServiceURL",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "AttributeConsumingServiceIndex",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "ProviderName",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "Issuer",
min_occurs: 0,
shape: Some(&SHAPE_ISSUER),
},
ChildShape {
namespace: Some(DS_NS),
local: "Signature",
min_occurs: 0,
shape: None,
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "Extensions",
min_occurs: 0,
shape: Some(&SHAPE_EXTENSIONS),
},
ChildShape {
namespace: Some(SAML_NS),
local: "Subject",
min_occurs: 0,
shape: Some(&SHAPE_SUBJECT),
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "NameIDPolicy",
min_occurs: 0,
shape: Some(&SHAPE_NAMEID_POLICY),
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "Conditions",
min_occurs: 0,
shape: Some(&SHAPE_REQUEST_CONDITIONS),
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "RequestedAuthnContext",
min_occurs: 0,
shape: Some(&SHAPE_REQUESTED_AUTHN_CONTEXT),
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "Scoping",
min_occurs: 0,
shape: Some(&SHAPE_SCOPING),
},
]),
wildcard_namespaces: &[],
};
static SHAPE_SESSION_INDEX: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "SessionIndex",
attrs: &[],
children: ChildModel::Any,
wildcard_namespaces: &[],
};
static SHAPE_LOGOUT_REQUEST: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "LogoutRequest",
attrs: &[
AttrShape {
namespace: None,
local: "ID",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "Version",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "IssueInstant",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "Destination",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "Consent",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "NotOnOrAfter",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "Reason",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "Issuer",
min_occurs: 0,
shape: Some(&SHAPE_ISSUER),
},
ChildShape {
namespace: Some(DS_NS),
local: "Signature",
min_occurs: 0,
shape: None,
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "Extensions",
min_occurs: 0,
shape: Some(&SHAPE_EXTENSIONS),
},
ChildShape {
namespace: Some(SAML_NS),
local: "NameID",
min_occurs: 0,
shape: Some(&SHAPE_NAMEID),
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "SessionIndex",
min_occurs: 0,
shape: Some(&SHAPE_SESSION_INDEX),
},
]),
wildcard_namespaces: &[SAML_NS],
};
static SHAPE_LOGOUT_RESPONSE: ElementShape = ElementShape {
namespace: Some(SAMLP_NS),
local: "LogoutResponse",
attrs: &[
AttrShape {
namespace: None,
local: "ID",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "Version",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "IssueInstant",
presence: AttrPresence::Required,
},
AttrShape {
namespace: None,
local: "InResponseTo",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "Destination",
presence: AttrPresence::Optional,
},
AttrShape {
namespace: None,
local: "Consent",
presence: AttrPresence::Optional,
},
],
children: ChildModel::Sequence(&[
ChildShape {
namespace: Some(SAML_NS),
local: "Issuer",
min_occurs: 0,
shape: Some(&SHAPE_ISSUER),
},
ChildShape {
namespace: Some(DS_NS),
local: "Signature",
min_occurs: 0,
shape: None,
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "Extensions",
min_occurs: 0,
shape: Some(&SHAPE_EXTENSIONS),
},
ChildShape {
namespace: Some(SAMLP_NS),
local: "Status",
min_occurs: 1,
shape: Some(&SHAPE_STATUS),
},
]),
wildcard_namespaces: &[],
};
pub(crate) fn validate_response(element: &Element) -> Result<(), Error> {
validate_against_shape(element, &SHAPE_RESPONSE)
}
pub(crate) fn validate_assertion(element: &Element) -> Result<(), Error> {
validate_against_shape(element, &SHAPE_ASSERTION)
}
pub(crate) fn validate_authn_request(element: &Element) -> Result<(), Error> {
validate_against_shape(element, &SHAPE_AUTHN_REQUEST)
}
#[cfg(feature = "slo")]
pub(crate) fn validate_logout_request(element: &Element) -> Result<(), Error> {
validate_against_shape(element, &SHAPE_LOGOUT_REQUEST)
}
#[cfg(feature = "slo")]
pub(crate) fn validate_logout_response(element: &Element) -> Result<(), Error> {
validate_against_shape(element, &SHAPE_LOGOUT_RESPONSE)
}
fn qname_string(q: &QName) -> String {
match q.namespace() {
Some(ns) => format!("{{{}}}{}", ns, q.local()),
None => q.local().to_owned(),
}
}
fn matches(q: &QName, ns: Option<&str>, local: &str) -> bool {
q.local() == local && q.namespace() == ns
}
fn validate_against_shape(element: &Element, shape: &ElementShape) -> Result<(), Error> {
if !matches(element.qname(), shape.namespace, shape.local) {
return Err(Error::SchemaViolation {
element: qname_string(element.qname()),
reason: "element name does not match expected SAML shape",
});
}
for attr in shape.attrs {
if attr.presence == AttrPresence::Required
&& element.attribute(attr.namespace, attr.local).is_none()
{
return Err(Error::SchemaViolation {
element: qname_string(element.qname()),
reason: missing_attr_reason(attr.local),
});
}
}
match shape.children {
ChildModel::Any => Ok(()),
ChildModel::Sequence(slots) => walk_sequence(element, slots, shape.wildcard_namespaces),
ChildModel::Choice(slots) => walk_choice(element, slots, shape.wildcard_namespaces),
}
}
fn walk_sequence(
parent: &Element,
slots: &[ChildShape],
wildcard_namespaces: &[&str],
) -> Result<(), Error> {
let mut cursor: usize = 0;
let mut counts: Vec<u32> = vec![0; slots.len()];
for child in parent.child_elements() {
let cq = child.qname();
let matched_slot = slots
.iter()
.enumerate()
.find(|(_, s)| matches(cq, s.namespace, s.local));
match matched_slot {
Some((idx, slot)) => {
if idx < cursor {
return Err(Error::SchemaViolation {
element: qname_string(parent.qname()),
reason: "child element appears out of schema order",
});
}
cursor = idx;
if let Some(inc) = counts.get_mut(idx) {
*inc = inc.saturating_add(1);
}
if let Some(sub_shape) = slot.shape {
validate_against_shape(child, sub_shape)?;
}
}
None => {
let ns = cq.namespace();
let allow_wildcard = ns.is_some_and(|n| wildcard_namespaces.contains(&n));
if !allow_wildcard {
return Err(Error::SchemaViolation {
element: qname_string(parent.qname()),
reason: "unexpected child element for SAML schema shape",
});
}
}
}
}
for (slot, count) in slots.iter().zip(counts.iter()) {
if *count < slot.min_occurs {
return Err(Error::SchemaViolation {
element: qname_string(parent.qname()),
reason: missing_child_reason(slot.local),
});
}
}
Ok(())
}
fn walk_choice(
parent: &Element,
slots: &[ChildShape],
wildcard_namespaces: &[&str],
) -> Result<(), Error> {
let mut any_matched = false;
for child in parent.child_elements() {
let cq = child.qname();
let matched = slots.iter().find(|s| matches(cq, s.namespace, s.local));
match matched {
Some(slot) => {
any_matched = true;
if let Some(sub_shape) = slot.shape {
validate_against_shape(child, sub_shape)?;
}
}
None => {
let ns = cq.namespace();
let allow_wildcard = ns.is_some_and(|n| wildcard_namespaces.contains(&n));
if !allow_wildcard {
return Err(Error::SchemaViolation {
element: qname_string(parent.qname()),
reason: "unexpected child element for SAML schema shape",
});
}
}
}
}
if !any_matched && !slots.is_empty() {
return Err(Error::SchemaViolation {
element: qname_string(parent.qname()),
reason: "no child satisfies the schema choice",
});
}
Ok(())
}
fn missing_attr_reason(local: &str) -> &'static str {
match local {
"ID" => "missing required attribute @ID",
"Version" => "missing required attribute @Version",
"IssueInstant" => "missing required attribute @IssueInstant",
"Method" => "missing required attribute @Method",
"Name" => "missing required attribute @Name",
"AuthnInstant" => "missing required attribute @AuthnInstant",
"Value" => "missing required attribute @Value",
_ => "missing required attribute",
}
}
fn missing_child_reason(local: &str) -> &'static str {
match local {
"Issuer" => "missing required child <saml:Issuer>",
"Status" => "missing required child <samlp:Status>",
"StatusCode" => "missing required child <samlp:StatusCode>",
"Subject" => "missing required child <saml:Subject>",
"Audience" => "missing required child <saml:Audience>",
"AuthnContext" => "missing required child <saml:AuthnContext>",
"Attribute" => "missing required child <saml:Attribute>",
_ => "missing required child element",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xml::parse::Document;
fn parse(xml: &str) -> Document {
Document::parse(xml.as_bytes()).expect("parse")
}
const NS_DECL: &str = r#"xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion""#;
#[test]
fn valid_minimal_response_passes() {
let xml = format!(
r#"<samlp:Response {NS_DECL}
ID="_r" Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<saml:Issuer>idp</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
</samlp:Response>"#
);
let doc = parse(&xml);
validate_response(doc.root()).expect("schema");
}
#[test]
fn response_missing_status_rejected() {
let xml = format!(
r#"<samlp:Response {NS_DECL}
ID="_r" Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<saml:Issuer>idp</saml:Issuer>
</samlp:Response>"#
);
let doc = parse(&xml);
let err = validate_response(doc.root()).unwrap_err();
match err {
Error::SchemaViolation { element, reason } => {
assert!(element.contains("Response"), "got: {element}");
assert!(reason.contains("Status"), "got: {reason}");
}
other => panic!("expected SchemaViolation, got {other:?}"),
}
}
#[test]
fn response_missing_id_rejected() {
let xml = format!(
r#"<samlp:Response {NS_DECL}
Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
</samlp:Response>"#
);
let doc = parse(&xml);
let err = validate_response(doc.root()).unwrap_err();
match err {
Error::SchemaViolation { reason, .. } => {
assert!(reason.contains("@ID"), "got: {reason}");
}
other => panic!("expected SchemaViolation, got {other:?}"),
}
}
#[test]
fn response_unknown_child_rejected() {
let xml = format!(
r#"<samlp:Response {NS_DECL}
xmlns:bogus="urn:bogus"
ID="_r" Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<saml:Issuer>idp</saml:Issuer>
<bogus:NotARealElement/>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
</samlp:Response>"#
);
let doc = parse(&xml);
let err = validate_response(doc.root()).unwrap_err();
match err {
Error::SchemaViolation { reason, .. } => {
assert!(reason.contains("unexpected child"), "got: {reason}");
}
other => panic!("expected SchemaViolation, got {other:?}"),
}
}
#[test]
fn response_with_assertion_passes() {
let xml = format!(
r#"<samlp:Response {NS_DECL}
ID="_r" Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<saml:Issuer>idp</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion ID="_a" Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<saml:Issuer>idp</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">u@e</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData Recipient="https://sp/acs" NotOnOrAfter="2026-05-26T12:05:00Z"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2026-05-26T11:59:00Z" NotOnOrAfter="2026-05-26T12:10:00Z">
<saml:AudienceRestriction>
<saml:Audience>https://sp</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2026-05-26T11:59:30Z">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
</saml:Assertion>
</samlp:Response>"#
);
let doc = parse(&xml);
validate_response(doc.root()).expect("schema");
}
#[test]
fn assertion_missing_issuer_rejected() {
let xml = r#"<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_a" Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<saml:Subject><saml:NameID>u</saml:NameID></saml:Subject>
</saml:Assertion>"#;
let doc = parse(xml);
let err = validate_assertion(doc.root()).unwrap_err();
match err {
Error::SchemaViolation { reason, .. } => {
assert!(reason.contains("<saml:Issuer>"), "got: {reason}");
}
other => panic!("expected SchemaViolation, got {other:?}"),
}
}
#[test]
fn assertion_out_of_order_rejected() {
let xml = r#"<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_a" Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<saml:Subject><saml:NameID>u</saml:NameID></saml:Subject>
<saml:Issuer>idp</saml:Issuer>
</saml:Assertion>"#;
let doc = parse(xml);
let err = validate_assertion(doc.root()).unwrap_err();
match err {
Error::SchemaViolation { reason, .. } => {
assert!(reason.contains("out of schema order"), "got: {reason}");
}
other => panic!("expected SchemaViolation, got {other:?}"),
}
}
#[test]
fn authn_request_minimal_passes() {
let xml = format!(
r#"<samlp:AuthnRequest {NS_DECL}
ID="_a" Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<saml:Issuer>https://sp</saml:Issuer>
</samlp:AuthnRequest>"#
);
let doc = parse(&xml);
validate_authn_request(doc.root()).expect("schema");
}
#[test]
fn authn_request_missing_version_rejected() {
let xml = format!(
r#"<samlp:AuthnRequest {NS_DECL}
ID="_a" IssueInstant="2026-05-26T12:00:00Z">
<saml:Issuer>https://sp</saml:Issuer>
</samlp:AuthnRequest>"#
);
let doc = parse(&xml);
let err = validate_authn_request(doc.root()).unwrap_err();
match err {
Error::SchemaViolation { reason, .. } => {
assert!(reason.contains("@Version"), "got: {reason}");
}
other => panic!("expected SchemaViolation, got {other:?}"),
}
}
#[cfg(feature = "slo")]
#[test]
fn logout_request_minimal_passes() {
let xml = format!(
r#"<samlp:LogoutRequest {NS_DECL}
ID="_l" Version="2.0" IssueInstant="2026-05-26T12:00:00Z">
<saml:Issuer>https://sp</saml:Issuer>
<saml:NameID>u</saml:NameID>
</samlp:LogoutRequest>"#
);
let doc = parse(&xml);
validate_logout_request(doc.root()).expect("schema");
}
#[cfg(feature = "slo")]
#[test]
fn logout_response_missing_status_rejected() {
let xml = format!(
r#"<samlp:LogoutResponse {NS_DECL}
ID="_l" Version="2.0" IssueInstant="2026-05-26T12:00:00Z" InResponseTo="_r">
<saml:Issuer>https://idp</saml:Issuer>
</samlp:LogoutResponse>"#
);
let doc = parse(&xml);
let err = validate_logout_response(doc.root()).unwrap_err();
match err {
Error::SchemaViolation { reason, .. } => {
assert!(reason.contains("<samlp:Status>"), "got: {reason}");
}
other => panic!("expected SchemaViolation, got {other:?}"),
}
}
#[test]
fn conditions_extension_via_wildcard_accepted() {
let xml = r#"<saml:Conditions xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
NotBefore="2026-05-26T11:59:00Z"
NotOnOrAfter="2026-05-26T12:10:00Z">
<saml:AudienceRestriction><saml:Audience>https://sp</saml:Audience></saml:AudienceRestriction>
<saml:Condition xsi:type="ext:CustomCondition"/>
</saml:Conditions>"#;
let doc = parse(xml);
validate_against_shape(doc.root(), &SHAPE_CONDITIONS).expect("schema");
}
}