-module(hund_xml).
-include("../include/hund.hrl").
-include("../include/hund_xpath_macro.hrl").
-include_lib("xmerl/include/xmerl.hrl").
-type xml_thing() :: #xmlDocument{}.
-export(
[
to_xml/1,
sign_xml/3,
build_nsinfo/2,
decode_sp_metadata/1,
decode_authn_request/1,
decode_logout_request/1,
decode_logout_response/1
]
).
-spec to_xml(Data :: hund:saml_record()) -> #xmlElement{}.
to_xml(
#saml_idp_metadata{
entity_id = EntityId,
certificate = Certificate,
login_location = LoginLocation,
logout_location = LogoutLocation,
org = #saml_org{name = OrgName, display_name = OrgDisplayName, url = OrgUrl},
tech = #saml_contact{name = TechName, email = TechEmail},
signed_request = SignedRequest,
name_format = NameFormat
}
) ->
Ns =
#xmlNamespace{
nodes =
[
{"md", 'urn:oasis:names:tc:SAML:2.0:metadata'},
{"ds", 'http://www.w3.org/2000/09/xmldsig#'}
]
},
KeyDescriptor =
case is_binary(Certificate) of
false -> [];
true ->
[
#xmlElement{
name = 'md:KeyDescriptor',
attributes = [#xmlAttribute{name = use, value = "signing"}],
content =
[
#xmlElement{
name = 'ds:KeyInfo',
content =
[
#xmlElement{
name = 'ds:X509Data',
content =
[
#xmlElement{
name = 'ds:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(Certificate)}]
}
]
}
]
}
]
},
#xmlElement{
name = 'md:KeyDescriptor',
attributes = [#xmlAttribute{name = use, value = "encryption"}],
content =
[
#xmlElement{
name = 'ds:KeyInfo',
content =
[
#xmlElement{
name = 'ds:X509Data',
content =
[
#xmlElement{
name = 'ds:X509Certificate',
content = [#xmlText{value = base64:encode_to_string(Certificate)}]
}
]
}
]
}
]
}
]
end,
SSoElement =
case LoginLocation of
"" -> [];
_ ->
[
#xmlElement{
name = 'md:SingleSignOnService',
attributes =
[
#xmlAttribute{
name = 'Binding',
value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
#xmlAttribute{name = 'Location', value = LoginLocation},
#xmlAttribute{name = index, value = "0"},
#xmlAttribute{name = isDefault, value = "true"}
]
},
#xmlElement{
name = 'md:SingleSignOnService',
attributes =
[
#xmlAttribute{
name = 'Binding',
value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
#xmlAttribute{name = 'Location', value = LoginLocation},
#xmlAttribute{name = index, value = "1"}
]
}
]
end,
SLoElement =
case LogoutLocation of
undefined -> [];
_ ->
[
#xmlElement{
name = 'md:SingleLogoutService',
attributes =
[
#xmlAttribute{
name = 'Binding',
value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
#xmlAttribute{name = 'Location', value = LogoutLocation},
#xmlAttribute{name = index, value = "0"},
#xmlAttribute{name = isDefault, value = "true"}
]
},
#xmlElement{
name = 'md:SingleLogoutService',
attributes =
[
#xmlAttribute{
name = 'Binding',
value = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
#xmlAttribute{name = 'Location', value = LogoutLocation},
#xmlAttribute{name = index, value = "1"}
]
}
]
end,
OrgLocation =
[
#xmlElement{
name = 'md:Organization',
content =
lang_elems(#xmlElement{name = 'md:OrganizationName'}, OrgName)
++
lang_elems(#xmlElement{name = 'md:OrganizationDisplayName'}, OrgDisplayName)
++
lang_elems(#xmlElement{name = 'md:OrganizationURL'}, OrgUrl)
}
],
TechElement =
[
#xmlElement{
name = 'md:ContactPerson',
content =
[
#xmlElement{name = 'md:GivenName', content = [#xmlText{value = TechName}]},
#xmlElement{name = 'md:EmailAddress', content = [#xmlText{value = TechEmail}]}
],
attributes = [#xmlAttribute{name = contactType, value = "technical"}]
}
],
NameIdElement =
[
#xmlElement{
name = 'md:NameIDFormat',
content = [#xmlText{value = hund:rev_nameid_map(NameFormat)}]
}
],
IDPSSODescriptor =
[
#xmlElement{
name = 'md:IDPSSODescriptor',
content = KeyDescriptor ++ SSoElement ++ SLoElement ++ NameIdElement,
attributes =
[#xmlAttribute{name = 'WantAuthnRequestsSigned', value = atom_to_list(SignedRequest)}]
}
],
build_nsinfo(
Ns,
#xmlElement{
name = 'md:EntityDescriptor',
content = IDPSSODescriptor ++ OrgLocation ++ TechElement,
attributes =
[
(#xmlAttribute{name = 'Version', value = "2.0"})
#xmlAttribute{name = entityID, value = EntityId},
#xmlAttribute{name = 'xmlns:md', value = proplists:get_value(md, Ns#xmlNamespace.nodes)},
#xmlAttribute{name = 'xmlns:ds', value = proplists:get_value(ds, Ns#xmlNamespace.nodes)}
]
}
);
to_xml(
#saml_assertion{
issuer = Issuer,
status = Status,
recipient = Recipient,
conditions =
#saml_condition{
not_before = ConditionNotBefore,
not_on_or_after = ConditionNotOnOrAfter,
audience = Audience
},
attributes = Attributes,
authn =
#saml_authn{
authn_instant = AuthnInstant,
session_not_on_or_after = SessionNotOnOrAfter,
session_index = SessionIndex,
authn_class = AuthnClass
},
subject =
#saml_subject{
in_response_to = InResponseTo,
confirmation_method = ConfirmationMethod,
name = Name,
name_format = NameFormat,
not_on_or_after = Notonorafter,
sp_name_qualifier = SpNameQualifier
}
}
) ->
Ns =
#xmlNamespace{
nodes =
[
{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'},
{"xsi", 'http://www.w3.org/2001/XMLSchema-instance'},
{"xs", 'http://www.w3.org/2001/XMLSchema'}
]
},
IssuerElement = [#xmlElement{name = 'saml:Issuer', content = [#xmlText{value = Issuer}]}],
StatusElement =
[
#xmlElement{
name = 'samlp:Status',
content =
[
#xmlElement{
name = 'samlp:StatusCode',
attributes = [#xmlAttribute{name = 'Value', value = hund:rev_status_code_map(Status)}]
}
]
}
],
AssertionSubjectElement =
[
#xmlElement{
name = 'saml:Subject',
content =
[
#xmlElement{
name = 'saml:NameID',
content = [#xmlText{value = Name}],
attributes =
[
#xmlAttribute{name = 'SPNameQualifier', value = SpNameQualifier},
#xmlAttribute{name = 'Format', value = hund:rev_nameid_map(NameFormat)}
]
},
#xmlElement{
name = 'saml:SubjectConfirmation',
attributes =
[
#xmlAttribute{
name = 'Method',
value = hund:rev_subject_method_map(ConfirmationMethod)
}
],
content =
[
#xmlElement{
name = 'saml:SubjectConfirmationData',
attributes =
[
#xmlAttribute{
name = 'NotOnOrAfter',
value = hund:datetime_to_saml(Notonorafter)
},
#xmlAttribute{name = 'Recipient', value = Recipient},
#xmlAttribute{name = 'InResponseTo', value = InResponseTo}
]
}
]
}
]
}
],
ConditionElement =
[
#xmlElement{
name = 'saml:Conditions',
attributes =
[
#xmlAttribute{name = 'NotBefore', value = hund:datetime_to_saml(ConditionNotBefore)},
#xmlAttribute{
name = 'NotOnOrAfter',
value = hund:datetime_to_saml(ConditionNotOnOrAfter)
}
],
content =
[
#xmlElement{
name = 'saml:AudienceRestriction',
content =
[
#xmlElement{
name = 'saml:Audience',
content = [#xmlText{value = stringify(Audience)}]
}
]
}
]
}
],
AuthnStatement =
[
#xmlElement{
name = 'saml:AuthnStatement',
attributes =
[
#xmlAttribute{name = 'AuthnInstant', value = hund:datetime_to_saml(AuthnInstant)},
#xmlAttribute{name = 'SessionIndex', value = SessionIndex},
#xmlAttribute{
name = 'SessionNotOnOrAfter',
value = hund:datetime_to_saml(SessionNotOnOrAfter)
}
],
content =
[
#xmlElement{
name = 'saml:AuthnContext',
content =
[
#xmlElement{
name = 'saml:AuthnContextClassRef',
content = [#xmlText{value = hund:rev_map_authn_class(AuthnClass)}]
}
]
}
]
}
],
AttributeStatement =
[
#xmlElement{
name = 'saml:AttributeStatement',
content =
[
#xmlElement{
name = 'saml:Attribute',
attributes =
[
#xmlAttribute{name = 'Name', value = Key},
#xmlAttribute{
name = 'NameFormat',
value = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
}
],
content =
[
#xmlElement{
name = 'saml:AttributeValue',
attributes = [#xmlAttribute{name = 'xsi:type', value = xml_type_schema(Value)}],
content = [#xmlText{value = stringify(Value)}]
}
]
}
|| {Key, Value} <- Attributes
]
}
],
Assertion =
[
#xmlElement{
name = 'saml:Assertion',
content =
IssuerElement
++
AssertionSubjectElement
++
ConditionElement
++
AuthnStatement
++
AttributeStatement,
attributes =
[
#xmlAttribute{name = 'Version', value = '2.0'},
#xmlAttribute{
name = 'IssueInstant',
value = hund:datetime_to_saml(calendar:universal_time())
}
]
}
],
build_nsinfo(
Ns,
#xmlElement{
name = 'samlp:Response',
content = IssuerElement ++ StatusElement ++ Assertion,
attributes =
[
#xmlAttribute{name = 'Version', value = "2.0"},
#xmlAttribute{name = 'xmlns:samlp', value = "urn:oasis:names:tc:SAML:2.0:protocol"},
#xmlAttribute{name = 'xmlns:saml', value = "urn:oasis:names:tc:SAML:2.0:assertion"},
#xmlAttribute{name = 'xmlns:xsi', value = "http://www.w3.org/2001/XMLSchema-instance"},
#xmlAttribute{name = 'xmlns:xs', value = "http://www.w3.org/2001/XMLSchema"}
]
}
);
to_xml(
#saml_logout_response{
destination = Destination,
in_response_to = InResponseTo,
issuer = Issuer,
status = Status,
issue_instant = IssueInstant
}
) ->
Ns =
#xmlNamespace{
nodes =
[
{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}
]
},
esaml_util:build_nsinfo(
Ns,
#xmlElement{
name = 'samlp:LogoutResponse',
attributes =
[
#xmlAttribute{name = 'xmlns:samlp', value = "urn:oasis:names:tc:SAML:2.0:protocol"},
#xmlAttribute{name = 'xmlns:saml', value = "urn:oasis:names:tc:SAML:2.0:assertion"},
#xmlAttribute{name = 'Destination', value = Destination},
#xmlAttribute{name = 'InResponseTo', value = InResponseTo},
#xmlAttribute{name = 'IssueInstant', value = hund:datetime_to_saml(IssueInstant)},
#xmlAttribute{name = 'Version', value = "2.0"}
],
content =
[
#xmlElement{name = 'saml:Issuer', content = [#xmlText{value = Issuer}]},
#xmlElement{
name = 'samlp:Status',
content =
[
#xmlElement{
name = 'samlp:StatusCode',
attributes =
[#xmlAttribute{name = 'Value', value = hund:rev_status_code_map(Status)}]
}
]
}
]
}
);
to_xml(
#saml_logout_request{
issue_instant = IssueInstant,
issuer = Issuer,
sp_name_qualifier = SpNameQualifier,
name_format = NameFormat,
name = Name,
destination = Destination
}
) ->
Ns =
#xmlNamespace{
nodes =
[
{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'}
]
},
build_nsinfo(
Ns,
#xmlElement{
name = 'samlp:LogoutRequest',
attributes =
[
#xmlAttribute{name = 'xmlns:samlp', value = "urn:oasis:names:tc:SAML:2.0:protocol"},
#xmlAttribute{name = 'xmlns:saml', value = "urn:oasis:names:tc:SAML:2.0:assertion"},
#xmlAttribute{name = 'Destination', value = Destination},
#xmlAttribute{name = 'IssueInstant', value = hund:datetime_to_saml(IssueInstant)},
#xmlAttribute{name = 'Version', value = "2.0"}
],
content =
[
#xmlElement{name = 'saml:Issuer', content = [#xmlText{value = Issuer}]},
#xmlElement{
name = 'saml:NameID',
attributes =
[
#xmlAttribute{name = 'SPNameQualifier', value = SpNameQualifier},
#xmlAttribute{name = 'Format', value = hund:rev_nameid_map(NameFormat)}
],
content = [#xmlText{value = Name}]
}
]
}
).
-spec sign_xml(Xml :: #xmlDocument{}, Path :: string(), fun((xml_thing()) -> xml_thing())) ->
{ok, #xmlDocument{}} | {error, term()}.
sign_xml(Xml, Path, F) ->
Ns =
#xmlNamespace{
nodes =
[
{"samlp", 'urn:oasis:names:tc:SAML:2.0:protocol'},
{"saml", 'urn:oasis:names:tc:SAML:2.0:assertion'},
{"xsi", 'http://www.w3.org/2001/XMLSchema-instance'},
{"xs", 'http://www.w3.org/2001/XMLSchema'}
]
},
case string:tokens(Path, "/") of
[] -> F(Xml);
[_Root] -> F(Xml);
Paths ->
Last = lists:last(Paths),
Rest = lists:droplast(Paths),
NewPath = lists:flatten("/" ++ lists:join("/", Rest) ++ "/*[not(self::" ++ Last ++ ")]"),
OtherEl =
case xmerl_xpath:string(NewPath, Xml, [{namespace, Ns}]) of
undefined -> [];
[] -> [];
Result -> Result
end,
case xmerl_xpath:string(Path, Xml, [{namespace, Ns}]) of
[Element] ->
Res = F(Element),
{ok, Xml#xmlElement{content = [Res] ++ OtherEl}};
_ -> {error, invalid_xpath}
end
end.
-spec decode_sp_metadata(Doc :: string() | #xmlElement{}) ->
{ok, #saml_sp_metadata{}} | {error, term()}.
decode_sp_metadata(Doc) when is_list(Doc) ->
{Xml, _Rest} = xmerl_scan:string(Doc, [{namespace_conformant, true}]),
decode_sp_metadata(Xml);
decode_sp_metadata(Xml = #xmlElement{}) ->
Ns = [{md, "urn:oasis:names:tc:SAML:2.0:metadata"}, {ds, "http://www.w3.org/2000/09/xmldsig#"}],
hund:threaduntil(
[
?xpath_attr_required(
"/md:EntityDescriptor/@entityID",
saml_sp_metadata,
entity_id,
bad_entity
),
?xpath_attr_required(
"/md:EntityDescriptor/md:SPSSODescriptor/md:AssertionConsumerService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']/@Location",
saml_sp_metadata,
consumer_location,
missing_consumer_location
),
?xpath_attr(
"/md:EntityDescriptor/md:SPSSODescriptor/@AuthnRequestsSigned",
saml_sp_metadata,
signed_request,
fun list_to_atom/1
),
?xpath_attr(
"/md:EntityDescriptor/md:SPSSODescriptor/@WantAssertionsSigned",
saml_sp_metadata,
signed_assertion,
fun list_to_atom/1
),
?xpath_attr(
"/md:EntityDescriptor/md:SPSSODescriptor/md:SingleLogoutService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']/@Location",
saml_sp_metadata,
logout_location
),
?xpath_text(
"/md:EntityDescriptor/md:SPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate/text()",
saml_sp_metadata,
certificate,
fun base64:decode_to_string/1
),
?xpath_recurse(
"/md:EntityDescriptor/md:Organization",
saml_sp_metadata,
org,
fun decode_org/1
),
?xpath_recurse(
"/md:EntityDescriptor/md:ContactPerson[@contactType='technical']",
saml_sp_metadata,
tech,
fun decode_contact_person/1
)
],
#saml_sp_metadata{}
).
-spec decode_authn_request(Doc :: string() | #xmlElement{}) ->
{ok, #saml_authnreq{}}
| {error, bad_issuer}
| {error, missing_consumer_location}
| {error, term()}.
decode_authn_request(Doc) when is_list(Doc) ->
{Xml, _Rest} = xmerl_scan:string(Doc, [{namespace_conformant, true}]),
decode_authn_request(Xml);
decode_authn_request(Xml = #xmlElement{}) ->
Ns =
[
{samlp, "urn:oasis:names:tc:SAML:2.0:protocol"},
{saml, "urn:oasis:names:tc:SAML:2.0:assertion"},
{ds, "http://www.w3.org/2000/09/xmldsig#"}
],
hund:threaduntil(
[
?xpath_text_required(
"/samlp:AuthnRequest/saml:Issuer/text()",
saml_authnreq,
issuer,
bad_issuer
),
?xpath_attr_required(
"/samlp:AuthnRequest/@AssertionConsumerServiceURL",
saml_authnreq,
consumer_location,
missing_consumer_location
),
?xpath_text(
"/samlp:AuthnRequest/samlp:RequestedAuthnContext/saml:AuthnContextClassRef/text()",
saml_authnreq,
authn_class,
fun hund:map_authn_class/1
),
?xpath_attr(
"/samlp:AuthnRequest/samlp:NameIDPolicy/@Format",
saml_authnreq,
name_format,
fun hund:nameid_map/1
),
?xpath_attr("/samlp:AuthnRequest/@Version", saml_authnreq, version),
?xpath_attr(
"/samlp:AuthnRequest/@IssueInstant",
saml_authnreq,
issue_instant,
fun hund:saml_to_datetime/1
),
?xpath_attr("/samlp:AuthnRequest/@Destination", saml_authnreq, destination)
],
#saml_authnreq{}
).
-spec decode_logout_request(Doc :: string() | #xmlElement{}) ->
{ok, #saml_logout_request{}} | {error, bad_issuer} | {error, term()}.
decode_logout_request(Doc) when is_list(Doc) ->
{Xml, _Rest} = xmerl_scan:string(Doc, [{namespace_conformant, true}]),
decode_logout_request(Xml);
decode_logout_request(Xml = #xmlElement{}) ->
Ns =
[
{samlp, "urn:oasis:names:tc:SAML:2.0:protocol"},
{saml, "urn:oasis:names:tc:SAML:2.0:assertion"},
{ds, "http://www.w3.org/2000/09/xmldsig#"}
],
hund:threaduntil(
[
?xpath_text_required(
"/samlp:LogoutRequest/saml:Issuer/text()",
saml_logout_request,
issuer,
bad_issuer
),
?xpath_attr(
"/samlp:LogoutRequest/saml:NameID/@Format",
saml_logout_request,
name_format,
fun hund:nameid_map/1
),
?xpath_text("/samlp:LogoutRequest/saml:NameID/text()", saml_logout_request, name),
?xpath_attr(
"/samlp:LogoutRequest/saml:NameID/@SPNameQualifier",
saml_logout_request,
sp_name_qualifier
),
?xpath_attr(
"/samlp:LogoutRequest/@IssueInstant",
saml_logout_request,
issue_instant,
fun hund:saml_to_datetime/1
),
?xpath_attr("/samlp:LogoutResponse/@Destination", saml_logout_response, destination),
?xpath_text(
"/samlp:LogoutRequest/samlp:SessionIndex/text()",
saml_logout_request,
session_index
)
],
#saml_logout_request{}
).
-spec decode_logout_response(Doc :: string() | #xmlElement{}) ->
{ok, #saml_logout_request{}} | {error, bad_issuer} | {error, term()}.
decode_logout_response(Doc) when is_list(Doc) ->
{Xml, _Rest} = xmerl_scan:string(Doc, [{namespace_conformant, true}]),
decode_logout_response(Xml);
decode_logout_response(Xml = #xmlElement{}) ->
Ns =
[
{samlp, "urn:oasis:names:tc:SAML:2.0:protocol"},
{saml, "urn:oasis:names:tc:SAML:2.0:assertion"},
{ds, "http://www.w3.org/2000/09/xmldsig#"}
],
hund:threaduntil(
[
?xpath_text_required(
"/samlp:LogoutResponse/saml:Issuer/text()",
saml_logout_response,
issuer,
bad_issuer
),
?xpath_attr(
"/samlp:LogoutResponse/@IssueInstant",
saml_logout_response,
issue_instant,
fun hund:saml_to_datetime/1
),
?xpath_attr("/samlp:LogoutResponse/@Destination", saml_logout_response, destination),
?xpath_attr("/samlp:LogoutResponse/@InResponseTo", saml_logout_response, in_response_to),
?xpath_attr(
"/samlp:LogoutResponse/samlp:Status/samlp:StatusCode/@Value",
saml_logout_response,
status,
fun hund:status_code_map/1
)
],
#saml_logout_response{}
).
-spec build_nsinfo(#xmlNamespace{}, #xmlElement{}) -> #xmlElement{}.
build_nsinfo(Ns, Attr = #xmlAttribute{name = Name}) ->
case string:tokens(atom_to_list(Name), ":") of
[NsPrefix, Rest] -> Attr#xmlAttribute{namespace = Ns, nsinfo = {NsPrefix, Rest}};
_ -> Attr#xmlAttribute{namespace = Ns}
end;
build_nsinfo(Ns, Elem = #xmlElement{name = Name, content = Children, attributes = Attrs}) ->
Elem2 =
case string:tokens(atom_to_list(Name), ":") of
[NsPrefix, Rest] -> Elem#xmlElement{namespace = Ns, nsinfo = {NsPrefix, Rest}};
_ -> Elem#xmlElement{namespace = Ns}
end,
Elem2#xmlElement{
attributes = [build_nsinfo(Ns, Attr) || Attr <- Attrs],
content = [build_nsinfo(Ns, Child) || Child <- Children]
};
build_nsinfo(_Ns, Other) -> Other.
-spec lang_elems(#xmlElement{}, hund:localized_string()) -> [#xmlElement{}].
lang_elems(BaseTag, Vals = [{Lang, _} | _]) when is_atom(Lang) ->
[
BaseTag#xmlElement{
attributes =
BaseTag#xmlElement.attributes
++
[#xmlAttribute{name = 'xml:lang', value = atom_to_list(L)}],
content = BaseTag#xmlElement.content ++ [#xmlText{value = V}]
}
|| {L, V} <- Vals
];
lang_elems(BaseTag, Val) ->
[
BaseTag#xmlElement{
attributes =
BaseTag#xmlElement.attributes ++ [#xmlAttribute{name = 'xml:lang', value = "en"}],
content = BaseTag#xmlElement.content ++ [#xmlText{value = Val}]
}
].
-spec xml_type_schema(Scalar :: term()) -> atom().
xml_type_schema(Scalar) when is_integer(Scalar) -> 'xs:integer';
xml_type_schema(Scalar) when is_boolean(Scalar) -> 'xs:boolean';
xml_type_schema(Scalar) when is_float(Scalar) -> 'xs:float';
xml_type_schema({{Year, Month, Day}})
when is_integer(Year) andalso is_integer(Month) andalso is_integer(Day) ->
'xs:date';
xml_type_schema({{Year, Month, Day}, {Hour, Minute, Second}})
when is_integer(Year)
andalso is_integer(Month) and is_integer(Day)
andalso is_integer(Hour)
andalso is_integer(Minute)
andalso is_integer(Second) ->
'xs:dateTime';
xml_type_schema(_) -> 'xs:string'.
-spec stringify(Scalar :: term()) -> string() | number().
stringify(Date = {Year, Month, Day})
when is_integer(Year) andalso is_integer(Month) andalso is_integer(Day) ->
hund:date_to_saml(Date);
stringify(DateTime = {{Year, Month, Day}, {Hour, Minute, Second}})
when is_integer(Year)
andalso is_integer(Month) and is_integer(Day)
andalso is_integer(Hour)
andalso is_integer(Minute)
andalso is_integer(Second) ->
hund:datetime_to_saml(DateTime);
stringify(Scalar) when is_atom(Scalar) -> atom_to_binary(Scalar);
stringify(Scalar) -> Scalar.
% private
-spec decode_org(Xml :: #xmlElement{}) -> #saml_org{}.
decode_org(Xml = #xmlElement{}) ->
Ns = [{md, "urn:oasis:names:tc:SAML:2.0:metadata"}, {ds, "http://www.w3.org/2000/09/xmldsig#"}],
hund:threaduntil(
[
?xpath_text("/md:Organization/md:OrganizationName/text()", saml_org, name),
?xpath_text("/md:Organization/md:OrganizationDisplayName/text()", saml_org, display_name),
?xpath_text("/md:Organization/md:OrganizationURL/text()", saml_org, url)
],
#saml_org{}
).
% private
-spec decode_contact_person(Xml :: #xmlElement{}) -> #saml_contact{}.
decode_contact_person(Xml = #xmlElement{}) ->
Ns = [{md, "urn:oasis:names:tc:SAML:2.0:metadata"}, {ds, "http://www.w3.org/2000/09/xmldsig#"}],
hund:threaduntil(
[
?xpath_text("/md:ContactPerson/md:GivenName/text()", saml_contact, name),
?xpath_text("/md:ContactPerson/md:EmailAddress/text()", saml_contact, email)
],
#saml_contact{}
).