1use crate::client::auth::no_auth::NO_AUTH_SCHEME_ID;
7use crate::client::identity::IdentityCache;
8use aws_smithy_runtime_api::box_error::BoxError;
9use aws_smithy_runtime_api::client::auth::{
10 AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, AuthSchemeOptionResolverParams,
11 ResolveAuthSchemeOptions,
12};
13use aws_smithy_runtime_api::client::identity::ResolveIdentity;
14use aws_smithy_runtime_api::client::identity::{IdentityCacheLocation, ResolveCachedIdentity};
15use aws_smithy_runtime_api::client::interceptors::context::InterceptorContext;
16use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
17use aws_smithy_types::config_bag::ConfigBag;
18use aws_smithy_types::endpoint::Endpoint;
19use aws_smithy_types::Document;
20use std::borrow::Cow;
21use std::error::Error as StdError;
22use std::fmt;
23use tracing::trace;
24
25#[derive(Debug)]
26struct NoMatchingAuthSchemeError(ExploredList);
27
28impl fmt::Display for NoMatchingAuthSchemeError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 let explored = &self.0;
31
32 if explored.items().count() == 0 {
35 return f.write_str(
36 "no auth options are available. This can happen if there's \
37 a problem with the service model, or if there is a codegen bug.",
38 );
39 }
40 if explored
41 .items()
42 .all(|explored| matches!(explored.result, ExploreResult::NoAuthScheme))
43 {
44 return f.write_str(
45 "no auth schemes are registered. This can happen if there's \
46 a problem with the service model, or if there is a codegen bug.",
47 );
48 }
49
50 let mut try_add_identity = false;
51 let mut likely_bug = false;
52 f.write_str("failed to select an auth scheme to sign the request with.")?;
53 for item in explored.items() {
54 write!(
55 f,
56 " \"{}\" wasn't a valid option because ",
57 item.scheme_id.as_str()
58 )?;
59 f.write_str(match item.result {
60 ExploreResult::NoAuthScheme => {
61 likely_bug = true;
62 "no auth scheme was registered for it."
63 }
64 ExploreResult::NoIdentityResolver => {
65 try_add_identity = true;
66 "there was no identity resolver for it."
67 }
68 ExploreResult::MissingEndpointConfig => {
69 likely_bug = true;
70 "there is auth config in the endpoint config, but this scheme wasn't listed in it \
71 (see https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details)."
72 }
73 ExploreResult::NotExplored => {
74 debug_assert!(false, "this should be unreachable");
75 "<unknown>"
76 }
77 })?;
78 }
79 if try_add_identity {
80 f.write_str(" Be sure to set an identity, such as credentials, auth token, or other identity type that is required for this service.")?;
81 } else if likely_bug {
82 f.write_str(" This is likely a bug.")?;
83 }
84 if explored.truncated {
85 f.write_str(" Note: there were other auth schemes that were evaluated that weren't listed here.")?;
86 }
87
88 Ok(())
89 }
90}
91
92impl StdError for NoMatchingAuthSchemeError {}
93
94#[derive(Debug)]
95enum AuthOrchestrationError {
96 MissingEndpointConfig,
97 BadAuthSchemeEndpointConfig(Cow<'static, str>),
98}
99
100impl fmt::Display for AuthOrchestrationError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match self {
103 Self::MissingEndpointConfig => f.write_str("missing endpoint config"),
105 Self::BadAuthSchemeEndpointConfig(message) => f.write_str(message),
106 }
107 }
108}
109
110impl StdError for AuthOrchestrationError {}
111
112pub(super) async fn orchestrate_auth(
113 ctx: &mut InterceptorContext,
114 runtime_components: &RuntimeComponents,
115 cfg: &ConfigBag,
116) -> Result<(), BoxError> {
117 let params = cfg
118 .load::<AuthSchemeOptionResolverParams>()
119 .expect("auth scheme option resolver params must be set");
120 let option_resolver = runtime_components.auth_scheme_option_resolver();
121 let options = option_resolver.resolve_auth_scheme_options(params)?;
122 let endpoint = cfg
123 .load::<Endpoint>()
124 .expect("endpoint added to config bag by endpoint orchestrator");
125
126 trace!(
127 auth_scheme_option_resolver_params = ?params,
128 auth_scheme_options = ?options,
129 "orchestrating auth",
130 );
131
132 let mut explored = ExploredList::default();
133
134 for &scheme_id in options.as_ref() {
136 if let Some(auth_scheme) = runtime_components.auth_scheme(scheme_id) {
138 if let Some(identity_resolver) = auth_scheme.identity_resolver(runtime_components) {
140 let identity_cache = if identity_resolver.cache_location()
141 == IdentityCacheLocation::RuntimeComponents
142 {
143 runtime_components.identity_cache()
144 } else {
145 IdentityCache::no_cache()
146 };
147 let signer = auth_scheme.signer();
148 trace!(
149 auth_scheme = ?auth_scheme,
150 identity_cache = ?identity_cache,
151 identity_resolver = ?identity_resolver,
152 signer = ?signer,
153 "resolved auth scheme, identity cache, identity resolver, and signing implementation"
154 );
155
156 match extract_endpoint_auth_scheme_config(endpoint, scheme_id) {
157 Ok(auth_scheme_endpoint_config) => {
158 trace!(auth_scheme_endpoint_config = ?auth_scheme_endpoint_config, "extracted auth scheme endpoint config");
159
160 let identity = identity_cache
161 .resolve_cached_identity(identity_resolver, runtime_components, cfg)
162 .await?;
163 trace!(identity = ?identity, "resolved identity");
164
165 trace!("signing request");
166 let request = ctx.request_mut().expect("set during serialization");
167 signer.sign_http_request(
168 request,
169 &identity,
170 auth_scheme_endpoint_config,
171 runtime_components,
172 cfg,
173 )?;
174 return Ok(());
175 }
176 Err(AuthOrchestrationError::MissingEndpointConfig) => {
177 explored.push(scheme_id, ExploreResult::MissingEndpointConfig);
178 continue;
179 }
180 Err(other_err) => return Err(other_err.into()),
181 }
182 } else {
183 explored.push(scheme_id, ExploreResult::NoIdentityResolver);
184 }
185 } else {
186 explored.push(scheme_id, ExploreResult::NoAuthScheme);
187 }
188 }
189
190 Err(NoMatchingAuthSchemeError(explored).into())
191}
192
193fn extract_endpoint_auth_scheme_config(
194 endpoint: &Endpoint,
195 scheme_id: AuthSchemeId,
196) -> Result<AuthSchemeEndpointConfig<'_>, AuthOrchestrationError> {
197 if scheme_id == NO_AUTH_SCHEME_ID {
200 return Ok(AuthSchemeEndpointConfig::empty());
201 }
202 let auth_schemes = match endpoint.properties().get("authSchemes") {
203 Some(Document::Array(schemes)) => schemes,
204 None => return Ok(AuthSchemeEndpointConfig::empty()),
206 _other => {
207 return Err(AuthOrchestrationError::BadAuthSchemeEndpointConfig(
208 "expected an array for `authSchemes` in endpoint config".into(),
209 ))
210 }
211 };
212 let auth_scheme_config = auth_schemes
213 .iter()
214 .find(|doc| {
215 let config_scheme_id = doc
216 .as_object()
217 .and_then(|object| object.get("name"))
218 .and_then(Document::as_string);
219 config_scheme_id == Some(scheme_id.as_str())
220 })
221 .ok_or(AuthOrchestrationError::MissingEndpointConfig)?;
222 Ok(AuthSchemeEndpointConfig::from(Some(auth_scheme_config)))
223}
224
225#[derive(Debug)]
226enum ExploreResult {
227 NotExplored,
228 NoAuthScheme,
229 NoIdentityResolver,
230 MissingEndpointConfig,
231}
232
233#[derive(Debug)]
236struct ExploredAuthOption {
237 scheme_id: AuthSchemeId,
238 result: ExploreResult,
239}
240impl Default for ExploredAuthOption {
241 fn default() -> Self {
242 Self {
243 scheme_id: AuthSchemeId::new(""),
244 result: ExploreResult::NotExplored,
245 }
246 }
247}
248
249const MAX_EXPLORED_LIST_LEN: usize = 8;
250
251#[derive(Default)]
253struct ExploredList {
254 items: [ExploredAuthOption; MAX_EXPLORED_LIST_LEN],
255 len: usize,
256 truncated: bool,
257}
258impl ExploredList {
259 fn items(&self) -> impl Iterator<Item = &ExploredAuthOption> {
260 self.items.iter().take(self.len)
261 }
262
263 fn push(&mut self, scheme_id: AuthSchemeId, result: ExploreResult) {
264 if self.len + 1 >= self.items.len() {
265 self.truncated = true;
266 } else {
267 self.items[self.len] = ExploredAuthOption { scheme_id, result };
268 self.len += 1;
269 }
270 }
271}
272impl fmt::Debug for ExploredList {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 f.debug_struct("ExploredList")
275 .field("items", &&self.items[0..self.len])
276 .field("truncated", &self.truncated)
277 .finish()
278 }
279}
280
281#[cfg(all(test, feature = "test-util"))]
282mod tests {
283 use super::*;
284 use aws_smithy_runtime_api::client::auth::static_resolver::StaticAuthSchemeOptionResolver;
285 use aws_smithy_runtime_api::client::auth::{
286 AuthScheme, AuthSchemeId, AuthSchemeOptionResolverParams, SharedAuthScheme,
287 SharedAuthSchemeOptionResolver, Sign,
288 };
289 use aws_smithy_runtime_api::client::identity::{
290 Identity, IdentityFuture, ResolveIdentity, SharedIdentityResolver,
291 };
292 use aws_smithy_runtime_api::client::interceptors::context::{Input, InterceptorContext};
293 use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
294 use aws_smithy_runtime_api::client::runtime_components::{
295 GetIdentityResolver, RuntimeComponents, RuntimeComponentsBuilder,
296 };
297 use aws_smithy_types::config_bag::Layer;
298 use std::collections::HashMap;
299
300 #[tokio::test]
301 async fn basic_case() {
302 #[derive(Debug)]
303 struct TestIdentityResolver;
304 impl ResolveIdentity for TestIdentityResolver {
305 fn resolve_identity<'a>(
306 &'a self,
307 _runtime_components: &'a RuntimeComponents,
308 _config_bag: &'a ConfigBag,
309 ) -> IdentityFuture<'a> {
310 IdentityFuture::ready(Ok(Identity::new("doesntmatter", None)))
311 }
312 }
313
314 #[derive(Debug)]
315 struct TestSigner;
316
317 impl Sign for TestSigner {
318 fn sign_http_request(
319 &self,
320 request: &mut HttpRequest,
321 _identity: &Identity,
322 _auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
323 _runtime_components: &RuntimeComponents,
324 _config_bag: &ConfigBag,
325 ) -> Result<(), BoxError> {
326 request
327 .headers_mut()
328 .insert(http_02x::header::AUTHORIZATION, "success!");
329 Ok(())
330 }
331 }
332
333 const TEST_SCHEME_ID: AuthSchemeId = AuthSchemeId::new("test-scheme");
334
335 #[derive(Debug)]
336 struct TestAuthScheme {
337 signer: TestSigner,
338 }
339 impl AuthScheme for TestAuthScheme {
340 fn scheme_id(&self) -> AuthSchemeId {
341 TEST_SCHEME_ID
342 }
343
344 fn identity_resolver(
345 &self,
346 identity_resolvers: &dyn GetIdentityResolver,
347 ) -> Option<SharedIdentityResolver> {
348 identity_resolvers.identity_resolver(self.scheme_id())
349 }
350
351 fn signer(&self) -> &dyn Sign {
352 &self.signer
353 }
354 }
355
356 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
357 ctx.enter_serialization_phase();
358 ctx.set_request(HttpRequest::empty());
359 let _ = ctx.take_input();
360 ctx.enter_before_transmit_phase();
361
362 let runtime_components = RuntimeComponentsBuilder::for_tests()
363 .with_auth_scheme(SharedAuthScheme::new(TestAuthScheme { signer: TestSigner }))
364 .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
365 StaticAuthSchemeOptionResolver::new(vec![TEST_SCHEME_ID]),
366 )))
367 .with_identity_resolver(
368 TEST_SCHEME_ID,
369 SharedIdentityResolver::new(TestIdentityResolver),
370 )
371 .build()
372 .unwrap();
373
374 let mut layer: Layer = Layer::new("test");
375 layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
376 layer.store_put(Endpoint::builder().url("dontcare").build());
377 let cfg = ConfigBag::of_layers(vec![layer]);
378
379 orchestrate_auth(&mut ctx, &runtime_components, &cfg)
380 .await
381 .expect("success");
382
383 assert_eq!(
384 "success!",
385 ctx.request()
386 .expect("request is set")
387 .headers()
388 .get("Authorization")
389 .unwrap()
390 );
391 }
392
393 #[cfg(feature = "http-auth")]
394 #[tokio::test]
395 async fn select_best_scheme_for_available_identity_resolvers() {
396 use crate::client::auth::http::{BasicAuthScheme, BearerAuthScheme};
397 use aws_smithy_runtime_api::client::auth::http::{
398 HTTP_BASIC_AUTH_SCHEME_ID, HTTP_BEARER_AUTH_SCHEME_ID,
399 };
400 use aws_smithy_runtime_api::client::identity::http::{Login, Token};
401
402 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
403 ctx.enter_serialization_phase();
404 ctx.set_request(HttpRequest::empty());
405 let _ = ctx.take_input();
406 ctx.enter_before_transmit_phase();
407
408 fn config_with_identity(
409 scheme_id: AuthSchemeId,
410 identity: impl ResolveIdentity + 'static,
411 ) -> (RuntimeComponents, ConfigBag) {
412 let runtime_components = RuntimeComponentsBuilder::for_tests()
413 .with_auth_scheme(SharedAuthScheme::new(BasicAuthScheme::new()))
414 .with_auth_scheme(SharedAuthScheme::new(BearerAuthScheme::new()))
415 .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
416 StaticAuthSchemeOptionResolver::new(vec![
417 HTTP_BASIC_AUTH_SCHEME_ID,
418 HTTP_BEARER_AUTH_SCHEME_ID,
419 ]),
420 )))
421 .with_identity_resolver(scheme_id, SharedIdentityResolver::new(identity))
422 .build()
423 .unwrap();
424
425 let mut layer = Layer::new("test");
426 layer.store_put(Endpoint::builder().url("dontcare").build());
427 layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
428
429 (runtime_components, ConfigBag::of_layers(vec![layer]))
430 }
431
432 let (runtime_components, cfg) =
434 config_with_identity(HTTP_BASIC_AUTH_SCHEME_ID, Login::new("a", "b", None));
435 orchestrate_auth(&mut ctx, &runtime_components, &cfg)
436 .await
437 .expect("success");
438 assert_eq!(
439 "Basic YTpi",
441 ctx.request()
442 .expect("request is set")
443 .headers()
444 .get("Authorization")
445 .unwrap()
446 );
447
448 let (runtime_components, cfg) =
450 config_with_identity(HTTP_BEARER_AUTH_SCHEME_ID, Token::new("t", None));
451 let mut ctx = InterceptorContext::new(Input::erase("doesnt-matter"));
452 ctx.enter_serialization_phase();
453 ctx.set_request(HttpRequest::empty());
454 let _ = ctx.take_input();
455 ctx.enter_before_transmit_phase();
456 orchestrate_auth(&mut ctx, &runtime_components, &cfg)
457 .await
458 .expect("success");
459 assert_eq!(
460 "Bearer t",
461 ctx.request()
462 .expect("request is set")
463 .headers()
464 .get("Authorization")
465 .unwrap()
466 );
467 }
468
469 #[test]
470 fn extract_endpoint_auth_scheme_config_no_config() {
471 let endpoint = Endpoint::builder()
472 .url("dontcare")
473 .property("something-unrelated", Document::Null)
474 .build();
475 let config = extract_endpoint_auth_scheme_config(&endpoint, "test-scheme-id".into())
476 .expect("success");
477 assert!(config.as_document().is_none());
478 }
479
480 #[test]
481 fn extract_endpoint_auth_scheme_config_wrong_type() {
482 let endpoint = Endpoint::builder()
483 .url("dontcare")
484 .property("authSchemes", Document::String("bad".into()))
485 .build();
486 extract_endpoint_auth_scheme_config(&endpoint, "test-scheme-id".into())
487 .expect_err("should fail because authSchemes is the wrong type");
488 }
489
490 #[test]
491 fn extract_endpoint_auth_scheme_config_no_matching_scheme() {
492 let endpoint = Endpoint::builder()
493 .url("dontcare")
494 .property(
495 "authSchemes",
496 vec![
497 Document::Object({
498 let mut out = HashMap::new();
499 out.insert("name".to_string(), "wrong-scheme-id".to_string().into());
500 out
501 }),
502 Document::Object({
503 let mut out = HashMap::new();
504 out.insert(
505 "name".to_string(),
506 "another-wrong-scheme-id".to_string().into(),
507 );
508 out
509 }),
510 ],
511 )
512 .build();
513 extract_endpoint_auth_scheme_config(&endpoint, "test-scheme-id".into())
514 .expect_err("should fail because authSchemes doesn't include the desired scheme");
515 }
516
517 #[test]
518 fn extract_endpoint_auth_scheme_config_successfully() {
519 let endpoint = Endpoint::builder()
520 .url("dontcare")
521 .property(
522 "authSchemes",
523 vec![
524 Document::Object({
525 let mut out = HashMap::new();
526 out.insert("name".to_string(), "wrong-scheme-id".to_string().into());
527 out
528 }),
529 Document::Object({
530 let mut out = HashMap::new();
531 out.insert("name".to_string(), "test-scheme-id".to_string().into());
532 out.insert(
533 "magicString".to_string(),
534 "magic string value".to_string().into(),
535 );
536 out
537 }),
538 ],
539 )
540 .build();
541 let config = extract_endpoint_auth_scheme_config(&endpoint, "test-scheme-id".into())
542 .expect("should find test-scheme-id");
543 assert_eq!(
544 "magic string value",
545 config
546 .as_document()
547 .expect("config is set")
548 .as_object()
549 .expect("it's an object")
550 .get("magicString")
551 .expect("magicString is set")
552 .as_string()
553 .expect("gimme the string, dammit!")
554 );
555 }
556
557 #[cfg(feature = "http-auth")]
558 #[tokio::test]
559 async fn use_identity_cache() {
560 use crate::client::auth::http::{ApiKeyAuthScheme, ApiKeyLocation};
561 use aws_smithy_runtime_api::client::auth::http::HTTP_API_KEY_AUTH_SCHEME_ID;
562 use aws_smithy_runtime_api::client::identity::http::Token;
563 use aws_smithy_types::body::SdkBody;
564
565 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
566 ctx.enter_serialization_phase();
567 ctx.set_request(
568 http_02x::Request::builder()
569 .body(SdkBody::empty())
570 .unwrap()
571 .try_into()
572 .unwrap(),
573 );
574 let _ = ctx.take_input();
575 ctx.enter_before_transmit_phase();
576
577 #[derive(Debug)]
578 struct Cache;
579 impl ResolveCachedIdentity for Cache {
580 fn resolve_cached_identity<'a>(
581 &'a self,
582 _resolver: SharedIdentityResolver,
583 _: &'a RuntimeComponents,
584 _config_bag: &'a ConfigBag,
585 ) -> IdentityFuture<'a> {
586 IdentityFuture::ready(Ok(Identity::new(Token::new("cached (pass)", None), None)))
587 }
588 }
589
590 let runtime_components = RuntimeComponentsBuilder::for_tests()
591 .with_auth_scheme(SharedAuthScheme::new(ApiKeyAuthScheme::new(
592 "result:",
593 ApiKeyLocation::Header,
594 "Authorization",
595 )))
596 .with_auth_scheme_option_resolver(Some(SharedAuthSchemeOptionResolver::new(
597 StaticAuthSchemeOptionResolver::new(vec![HTTP_API_KEY_AUTH_SCHEME_ID]),
598 )))
599 .with_identity_cache(Some(Cache))
600 .with_identity_resolver(
601 HTTP_API_KEY_AUTH_SCHEME_ID,
602 SharedIdentityResolver::new(Token::new("uncached (fail)", None)),
603 )
604 .build()
605 .unwrap();
606 let mut layer = Layer::new("test");
607 layer.store_put(Endpoint::builder().url("dontcare").build());
608 layer.store_put(AuthSchemeOptionResolverParams::new("doesntmatter"));
609 let config_bag = ConfigBag::of_layers(vec![layer]);
610
611 orchestrate_auth(&mut ctx, &runtime_components, &config_bag)
612 .await
613 .expect("success");
614 assert_eq!(
615 "result: cached (pass)",
616 ctx.request()
617 .expect("request is set")
618 .headers()
619 .get("Authorization")
620 .unwrap()
621 );
622 }
623
624 #[test]
625 fn friendly_error_messages() {
626 let err = NoMatchingAuthSchemeError(ExploredList::default());
627 assert_eq!(
628 "no auth options are available. This can happen if there's a problem with \
629 the service model, or if there is a codegen bug.",
630 err.to_string()
631 );
632
633 let mut list = ExploredList::default();
634 list.push(
635 AuthSchemeId::new("SigV4"),
636 ExploreResult::NoIdentityResolver,
637 );
638 list.push(
639 AuthSchemeId::new("SigV4a"),
640 ExploreResult::NoIdentityResolver,
641 );
642 let err = NoMatchingAuthSchemeError(list);
643 assert_eq!(
644 "failed to select an auth scheme to sign the request with. \
645 \"SigV4\" wasn't a valid option because there was no identity resolver for it. \
646 \"SigV4a\" wasn't a valid option because there was no identity resolver for it. \
647 Be sure to set an identity, such as credentials, auth token, or other identity \
648 type that is required for this service.",
649 err.to_string()
650 );
651
652 let mut list = ExploredList::default();
654 list.push(
655 AuthSchemeId::new("SigV4"),
656 ExploreResult::NoIdentityResolver,
657 );
658 list.push(
659 AuthSchemeId::new("SigV4a"),
660 ExploreResult::MissingEndpointConfig,
661 );
662 let err = NoMatchingAuthSchemeError(list);
663 assert_eq!(
664 "failed to select an auth scheme to sign the request with. \
665 \"SigV4\" wasn't a valid option because there was no identity resolver for it. \
666 \"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
667 config, but this scheme wasn't listed in it (see \
668 https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
669 Be sure to set an identity, such as credentials, auth token, or other identity \
670 type that is required for this service.",
671 err.to_string()
672 );
673
674 let mut list = ExploredList::default();
676 list.push(
677 AuthSchemeId::new("SigV4a"),
678 ExploreResult::MissingEndpointConfig,
679 );
680 let err = NoMatchingAuthSchemeError(list);
681 assert_eq!(
682 "failed to select an auth scheme to sign the request with. \
683 \"SigV4a\" wasn't a valid option because there is auth config in the endpoint \
684 config, but this scheme wasn't listed in it (see \
685 https://github.com/smithy-lang/smithy-rs/discussions/3281 for more details). \
686 This is likely a bug.",
687 err.to_string()
688 );
689
690 let mut list = ExploredList::default();
692 for _ in 0..=MAX_EXPLORED_LIST_LEN {
693 list.push(
694 AuthSchemeId::new("dontcare"),
695 ExploreResult::MissingEndpointConfig,
696 );
697 }
698 let err = NoMatchingAuthSchemeError(list).to_string();
699 if !err.contains(
700 "Note: there were other auth schemes that were evaluated that weren't listed here",
701 ) {
702 panic!("The error should indicate that the explored list was truncated.");
703 }
704 }
705}