1use crate::{
2 AUTHORIZATION_HEADER, AuthorizationCarrier, AuthorizationCarrierError, Command,
3 CredentialScheme, DEFAULT_HTTP_ENDPOINT_BASE, ErrorCode, InspectDocument,
4 ProtectedResourceAuthorization,
5};
6
7pub fn normalize_endpoint_base(endpoint_base: Option<&str>) -> Result<String, crate::CoreError> {
8 let endpoint_base = endpoint_base.unwrap_or(DEFAULT_HTTP_ENDPOINT_BASE);
9 if !endpoint_base.starts_with('/') || endpoint_base.starts_with("//") {
10 return Err(crate::CoreError::Invalid(
11 "AEP endpoint_base must be an origin-relative absolute path".to_owned(),
12 ));
13 }
14 if endpoint_base.ends_with('/') {
15 Ok(endpoint_base.to_owned())
16 } else {
17 Ok(format!("{endpoint_base}/"))
18 }
19}
20
21pub fn command_path(
22 command: &Command,
23 endpoint_base: Option<&str>,
24) -> Result<String, crate::CoreError> {
25 let relative = match command {
26 Command::Enroll => "enroll",
27 Command::Grant => "grant",
28 Command::Revoke => "revoke",
29 Command::Status => "status",
30 Command::Inspect | Command::Other(_) => {
31 return Err(crate::CoreError::Invalid(
32 "AEP command has no HTTP endpoint path".to_owned(),
33 ));
34 }
35 };
36 Ok(format!(
37 "{}{relative}",
38 normalize_endpoint_base(endpoint_base)?
39 ))
40}
41
42pub fn command_path_from_inspect(
43 document: &InspectDocument,
44 command: &Command,
45) -> Result<String, crate::CoreError> {
46 command_path(command, document.http.endpoint_base.as_deref())
47}
48
49pub const fn protected_resource_authorization_header(
50 carrier: AuthorizationCarrier,
51) -> &'static str {
52 match carrier {
53 AuthorizationCarrier::Standard => "Authorization",
54 AuthorizationCarrier::Dedicated => AUTHORIZATION_HEADER,
55 }
56}
57
58pub fn render_protected_resource_authorization(
59 value: &ProtectedResourceAuthorization,
60) -> Result<(String, String), AuthorizationCarrierError> {
61 validate_protected_resource_authorization(value)?;
62 Ok((
63 protected_resource_authorization_header(value.carrier).to_owned(),
64 format!("{} {}", value.scheme.as_str(), value.credentials),
65 ))
66}
67
68pub fn validate_protected_resource_authorization(
69 value: &ProtectedResourceAuthorization,
70) -> Result<(), AuthorizationCarrierError> {
71 if value.credentials.is_empty() {
72 return Err(AuthorizationCarrierError {
73 code: ErrorCode::InvalidRequest,
74 message: "authorization credentials must not be empty".to_owned(),
75 });
76 }
77 Ok(())
78}
79
80pub fn parse_protected_resource_authorization(
81 field_value: &str,
82 carrier: AuthorizationCarrier,
83) -> Result<ProtectedResourceAuthorization, AuthorizationCarrierError> {
84 if carrier == AuthorizationCarrier::Dedicated && field_value.contains(',') {
85 return Err(not_recognized(
86 "the dedicated authorization field is ambiguous",
87 ));
88 }
89 let Some((scheme, credentials)) = field_value.split_once(' ') else {
90 return Err(not_recognized(
91 "the authorization presentation was not recognized",
92 ));
93 };
94 if scheme.is_empty()
95 || credentials.is_empty()
96 || credentials.starts_with(' ')
97 || credentials.starts_with('\t')
98 {
99 return Err(not_recognized(
100 "the authorization presentation was not recognized",
101 ));
102 }
103 let scheme = if scheme.eq_ignore_ascii_case("aep") {
104 CredentialScheme::Aep
105 } else if scheme.eq_ignore_ascii_case("bearer") {
106 CredentialScheme::Bearer
107 } else if scheme.eq_ignore_ascii_case("basic") {
108 CredentialScheme::Basic
109 } else {
110 return Err(not_recognized(
111 "the authorization presentation was not recognized",
112 ));
113 };
114 Ok(ProtectedResourceAuthorization {
115 carrier,
116 scheme,
117 credentials: credentials.to_owned(),
118 })
119}
120
121fn not_recognized(message: &str) -> AuthorizationCarrierError {
122 AuthorizationCarrierError {
123 code: ErrorCode::NotRecognized,
124 message: message.to_owned(),
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use proptest::prelude::*;
131
132 use super::*;
133
134 #[test]
135 fn normalizes_command_paths() {
136 assert_eq!(
137 command_path(&Command::Enroll, Some("/custom")).expect("valid path"),
138 "/custom/enroll"
139 );
140 }
141
142 #[test]
143 fn rejects_ambiguous_dedicated_authorization() {
144 let error = parse_protected_resource_authorization(
145 "AEP first, AEP second",
146 AuthorizationCarrier::Dedicated,
147 )
148 .expect_err("combined dedicated field must fail");
149 assert_eq!(error.code, ErrorCode::NotRecognized);
150 }
151
152 #[test]
153 fn parses_and_renders_supported_authorization_schemes() {
154 for (field, rendered_field, scheme) in [
155 ("AEP assertion", "AEP assertion", CredentialScheme::Aep),
156 ("bearer token", "Bearer token", CredentialScheme::Bearer),
157 ("Basic value", "Basic value", CredentialScheme::Basic),
158 ] {
159 let parsed =
160 parse_protected_resource_authorization(field, AuthorizationCarrier::Standard)
161 .expect("recognized authorization");
162 assert_eq!(parsed.scheme, scheme);
163 let (header, rendered) =
164 render_protected_resource_authorization(&parsed).expect("rendered authorization");
165 assert_eq!(header, "Authorization");
166 assert_eq!(rendered, rendered_field);
167 }
168 assert!(
169 parse_protected_resource_authorization("Digest value", AuthorizationCarrier::Standard)
170 .is_err()
171 );
172 assert!(
173 validate_protected_resource_authorization(&ProtectedResourceAuthorization {
174 carrier: AuthorizationCarrier::Dedicated,
175 scheme: CredentialScheme::Aep,
176 credentials: String::new(),
177 })
178 .is_err()
179 );
180 }
181
182 #[test]
183 fn rejects_commands_without_http_paths() {
184 assert!(command_path(&Command::Inspect, None).is_err());
185 assert!(normalize_endpoint_base(Some("https://service.example/aep")).is_err());
186 }
187
188 proptest! {
189 #[test]
190 fn normalized_paths_always_end_in_a_single_separator(
191 segments in proptest::collection::vec("[a-z]{1,8}", 1..5)
192 ) {
193 let input = format!("/{}", segments.join("/"));
194 let normalized = normalize_endpoint_base(Some(&input)).expect("valid path");
195 prop_assert!(normalized.starts_with('/'));
196 prop_assert!(!normalized.starts_with("//"));
197 prop_assert!(normalized.ends_with('/'));
198 }
199 }
200}