From 1d1e68aeba46de72bfb516ec74377da3976b4f57 Mon Sep 17 00:00:00 2001 From: Eduardo de Moura Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Tue, 14 Feb 2023 00:07:06 +0100 Subject: [PATCH] feat(smithy-http-auth): add api key auth types (#2153) * feat(aws-types): add api key to configuration * chore: set package version to 0.52.0 * feat(aws-smithy-types): create auth types * chore: use auth from smithy types * chore: fix return self type * chore: create http auth definition type * chore: add constructor for http auth definition * chore: ensure properties are not moved * chore: create convenience constructors * chore: add some todo comments * feat: move query writer to aws-smithy-http crate * chore(codegen): expose smithy http tower dependency * chore: remove setters for auth definition * chore: fix logical error for scheme not allowed * chore: add constructor for basic and digest auth * chore: allow equality comparision for api key * Revert "chore: set package version to 0.52.0" This reverts commit da660fcf160e24605ebe082deb6f339213926340. * chore: fix additional references to querywriter * chore: implement from string for api key struct * chore: disallow none api key in sdk config * chore: fix formatting * chore: add unit tests for auth types * chore: add auth api key to external types * chore: make query writer doc hidden * feat: create aws-smithy-http-auth crate * chore: use zeroing for auth api key * chore: use builder pattern for auth definition * chore: restructure http auth package * chore: define default lint warning * chore: include http auth in runtime common list * chore: define setter for optional scheme * chore: should panic while building auth definition * chore: return missing required field error * chore: make few code simplications for api key * Revert "chore: add auth api key to external types" This reverts commit b2318b02304fdeefbe5cb8eba80047ae307d08fa. * chore: revert api key from sdk config * chore: panic on missing required field * Opt out of `clippy::derive_partial_eq_without_eq` --------- Co-authored-by: Eduardo Rodrigues Co-authored-by: John DiSanti --- .../src/http_request/canonical_request.rs | 4 +- .../aws-sigv4/src/http_request/mod.rs | 1 - .../aws-sigv4/src/http_request/sign.rs | 2 +- .../aws-sigv4/src/http_request/url_escape.rs | 6 +- buildSrc/src/main/kotlin/CrateSet.kt | 1 + .../rust/codegen/core/smithy/RuntimeType.kt | 1 + rust-runtime/Cargo.toml | 1 + rust-runtime/aws-smithy-http-auth/Cargo.toml | 20 ++ rust-runtime/aws-smithy-http-auth/LICENSE | 175 ++++++++++++ rust-runtime/aws-smithy-http-auth/README.md | 7 + .../aws-smithy-http-auth/external-types.toml | 2 + .../aws-smithy-http-auth/src/api_key.rs | 74 ++++++ .../aws-smithy-http-auth/src/definition.rs | 251 ++++++++++++++++++ .../aws-smithy-http-auth/src/error.rs | 42 +++ rust-runtime/aws-smithy-http-auth/src/lib.rs | 14 + .../aws-smithy-http-auth/src/location.rs | 73 +++++ rust-runtime/aws-smithy-http/src/lib.rs | 2 + .../aws-smithy-http/src}/query_writer.rs | 14 +- 18 files changed, 674 insertions(+), 16 deletions(-) create mode 100644 rust-runtime/aws-smithy-http-auth/Cargo.toml create mode 100644 rust-runtime/aws-smithy-http-auth/LICENSE create mode 100644 rust-runtime/aws-smithy-http-auth/README.md create mode 100644 rust-runtime/aws-smithy-http-auth/external-types.toml create mode 100644 rust-runtime/aws-smithy-http-auth/src/api_key.rs create mode 100644 rust-runtime/aws-smithy-http-auth/src/definition.rs create mode 100644 rust-runtime/aws-smithy-http-auth/src/error.rs create mode 100644 rust-runtime/aws-smithy-http-auth/src/lib.rs create mode 100644 rust-runtime/aws-smithy-http-auth/src/location.rs rename {aws/rust-runtime/aws-sigv4/src/http_request => rust-runtime/aws-smithy-http/src}/query_writer.rs (94%) diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs b/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs index 172f2cbce7..08ac6c8f54 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/canonical_request.rs @@ -5,7 +5,6 @@ use crate::date_time::{format_date, format_date_time}; use crate::http_request::error::CanonicalRequestError; -use crate::http_request::query_writer::QueryWriter; use crate::http_request::settings::UriPathNormalizationMode; use crate::http_request::sign::SignableRequest; use crate::http_request::uri_path_normalization::normalize_uri_path; @@ -13,6 +12,7 @@ use crate::http_request::url_escape::percent_encode_path; use crate::http_request::PercentEncodingMode; use crate::http_request::{PayloadChecksumKind, SignableBody, SignatureLocation, SigningParams}; use crate::sign::sha256_hex_string; +use aws_smithy_http::query_writer::QueryWriter; use http::header::{AsHeaderName, HeaderName, HOST}; use http::{HeaderMap, HeaderValue, Method, Uri}; use std::borrow::Cow; @@ -519,13 +519,13 @@ mod tests { use crate::http_request::canonical_request::{ normalize_header_value, trim_all, CanonicalRequest, SigningScope, StringToSign, }; - use crate::http_request::query_writer::QueryWriter; use crate::http_request::test::{test_canonical_request, test_request, test_sts}; use crate::http_request::{ PayloadChecksumKind, SignableBody, SignableRequest, SigningSettings, }; use crate::http_request::{SignatureLocation, SigningParams}; use crate::sign::sha256_hex_string; + use aws_smithy_http::query_writer::QueryWriter; use http::Uri; use http::{header::HeaderName, HeaderValue}; use pretty_assertions::assert_eq; diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs b/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs index c93a052887..543d58fb2d 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/mod.rs @@ -43,7 +43,6 @@ mod canonical_request; mod error; -mod query_writer; mod settings; mod sign; mod uri_path_normalization; diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs b/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs index d120a401e7..dbe7624294 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs @@ -8,10 +8,10 @@ use super::{PayloadChecksumKind, SignatureLocation}; use crate::http_request::canonical_request::header; use crate::http_request::canonical_request::param; use crate::http_request::canonical_request::{CanonicalRequest, StringToSign, HMAC_256}; -use crate::http_request::query_writer::QueryWriter; use crate::http_request::SigningParams; use crate::sign::{calculate_signature, generate_signing_key, sha256_hex_string}; use crate::SigningOutput; +use aws_smithy_http::query_writer::QueryWriter; use http::header::HeaderValue; use http::{HeaderMap, Method, Uri}; use std::borrow::Cow; diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/url_escape.rs b/aws/rust-runtime/aws-sigv4/src/http_request/url_escape.rs index 651c62c44a..d7656c355b 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/url_escape.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/url_escape.rs @@ -3,11 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -use aws_smithy_http::{label, query}; - -pub(super) fn percent_encode_query(value: &str) -> String { - query::fmt_string(value) -} +use aws_smithy_http::label; pub(super) fn percent_encode_path(value: &str) -> String { label::fmt_string(value, label::EncodingStrategy::Greedy) diff --git a/buildSrc/src/main/kotlin/CrateSet.kt b/buildSrc/src/main/kotlin/CrateSet.kt index c915649f61..90e1df95ab 100644 --- a/buildSrc/src/main/kotlin/CrateSet.kt +++ b/buildSrc/src/main/kotlin/CrateSet.kt @@ -21,6 +21,7 @@ object CrateSet { "aws-smithy-checksums", "aws-smithy-eventstream", "aws-smithy-http", + "aws-smithy-http-auth", "aws-smithy-http-tower", "aws-smithy-json", "aws-smithy-protocol-test", diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt index 50887da953..14c53f99e3 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt @@ -251,6 +251,7 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) fun smithyClient(runtimeConfig: RuntimeConfig) = CargoDependency.smithyClient(runtimeConfig).toType() fun smithyEventStream(runtimeConfig: RuntimeConfig) = CargoDependency.smithyEventStream(runtimeConfig).toType() fun smithyHttp(runtimeConfig: RuntimeConfig) = CargoDependency.smithyHttp(runtimeConfig).toType() + fun smithyHttpTower(runtimeConfig: RuntimeConfig) = CargoDependency.smithyHttpTower(runtimeConfig).toType() fun smithyJson(runtimeConfig: RuntimeConfig) = CargoDependency.smithyJson(runtimeConfig).toType() fun smithyQuery(runtimeConfig: RuntimeConfig) = CargoDependency.smithyQuery(runtimeConfig).toType() fun smithyTypes(runtimeConfig: RuntimeConfig) = CargoDependency.smithyTypes(runtimeConfig).toType() diff --git a/rust-runtime/Cargo.toml b/rust-runtime/Cargo.toml index 185d7082a0..6a53b080e9 100644 --- a/rust-runtime/Cargo.toml +++ b/rust-runtime/Cargo.toml @@ -7,6 +7,7 @@ members = [ "aws-smithy-checksums", "aws-smithy-eventstream", "aws-smithy-http", + "aws-smithy-http-auth", "aws-smithy-http-tower", "aws-smithy-json", "aws-smithy-protocol-test", diff --git a/rust-runtime/aws-smithy-http-auth/Cargo.toml b/rust-runtime/aws-smithy-http-auth/Cargo.toml new file mode 100644 index 0000000000..0d70b25eec --- /dev/null +++ b/rust-runtime/aws-smithy-http-auth/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "aws-smithy-http-auth" +version = "0.0.0-smithy-rs-head" +authors = [ + "AWS Rust SDK Team ", + "Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com>", +] +description = "Smithy HTTP logic for smithy-rs." +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/awslabs/smithy-rs" + +[dependencies] +zeroize = "1" + +[package.metadata.docs.rs] +all-features = true +targets = ["x86_64-unknown-linux-gnu"] +rustdoc-args = ["--cfg", "docsrs"] +# End of docs.rs metadata diff --git a/rust-runtime/aws-smithy-http-auth/LICENSE b/rust-runtime/aws-smithy-http-auth/LICENSE new file mode 100644 index 0000000000..67db858821 --- /dev/null +++ b/rust-runtime/aws-smithy-http-auth/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/rust-runtime/aws-smithy-http-auth/README.md b/rust-runtime/aws-smithy-http-auth/README.md new file mode 100644 index 0000000000..1d963cafce --- /dev/null +++ b/rust-runtime/aws-smithy-http-auth/README.md @@ -0,0 +1,7 @@ +# aws-smithy-http-auth + +HTTP Auth implementation for service clients generated by [smithy-rs](https://github.com/awslabs/smithy-rs). + + +This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/awslabs/smithy-rs) code generator. In most cases, it should not be used directly. + diff --git a/rust-runtime/aws-smithy-http-auth/external-types.toml b/rust-runtime/aws-smithy-http-auth/external-types.toml new file mode 100644 index 0000000000..ff30ccf5ad --- /dev/null +++ b/rust-runtime/aws-smithy-http-auth/external-types.toml @@ -0,0 +1,2 @@ +allowed_external_types = [ +] diff --git a/rust-runtime/aws-smithy-http-auth/src/api_key.rs b/rust-runtime/aws-smithy-http-auth/src/api_key.rs new file mode 100644 index 0000000000..bb2ab65b3c --- /dev/null +++ b/rust-runtime/aws-smithy-http-auth/src/api_key.rs @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! HTTP Auth API Key + +use std::cmp::PartialEq; +use std::fmt::Debug; +use std::sync::Arc; +use zeroize::Zeroizing; + +/// Authentication configuration to connect to a Smithy Service +#[derive(Clone, Eq, PartialEq)] +pub struct AuthApiKey(Arc); + +#[derive(Clone, Eq, PartialEq)] +struct Inner { + api_key: Zeroizing, +} + +impl Debug for AuthApiKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut auth_api_key = f.debug_struct("AuthApiKey"); + auth_api_key.field("api_key", &"** redacted **").finish() + } +} + +impl AuthApiKey { + /// Constructs a new API key. + pub fn new(api_key: impl Into) -> Self { + Self(Arc::new(Inner { + api_key: Zeroizing::new(api_key.into()), + })) + } + + /// Returns the underlying api key. + pub fn api_key(&self) -> &str { + &self.0.api_key + } +} + +impl From<&str> for AuthApiKey { + fn from(api_key: &str) -> Self { + Self::from(api_key.to_owned()) + } +} + +impl From for AuthApiKey { + fn from(api_key: String) -> Self { + Self(Arc::new(Inner { + api_key: Zeroizing::new(api_key), + })) + } +} + +#[cfg(test)] +mod tests { + use super::AuthApiKey; + + #[test] + fn api_key_is_equal() { + let api_key_a: AuthApiKey = "some-api-key".into(); + let api_key_b = AuthApiKey::new("some-api-key"); + assert_eq!(api_key_a, api_key_b); + } + + #[test] + fn api_key_is_different() { + let api_key_a = AuthApiKey::new("some-api-key"); + let api_key_b: AuthApiKey = String::from("another-api-key").into(); + assert_ne!(api_key_a, api_key_b); + } +} diff --git a/rust-runtime/aws-smithy-http-auth/src/definition.rs b/rust-runtime/aws-smithy-http-auth/src/definition.rs new file mode 100644 index 0000000000..918f6aae8f --- /dev/null +++ b/rust-runtime/aws-smithy-http-auth/src/definition.rs @@ -0,0 +1,251 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! HTTP Auth Definition + +use crate::location::HttpAuthLocation; +use std::cmp::PartialEq; +use std::fmt::Debug; + +/// A HTTP-specific authentication scheme that sends an arbitrary +/// auth value in a header or query string parameter. +// As described in the Smithy documentation: +// https://github.com/awslabs/smithy/blob/main/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +#[derive(Clone, Debug, Default, PartialEq)] +pub struct HttpAuthDefinition { + /// Defines the location of where the Auth is serialized. + location: HttpAuthLocation, + + /// Defines the name of the HTTP header or query string parameter + /// that contains the Auth. + name: String, + + /// Defines the security scheme to use on the `Authorization` header value. + /// This can only be set if the "location" property is set to [`HttpAuthLocation::Header`]. + scheme: Option, +} + +impl HttpAuthDefinition { + /// Returns a builder for `HttpAuthDefinition`. + pub fn builder() -> http_auth_definition::Builder { + http_auth_definition::Builder::default() + } + + /// Constructs a new HTTP auth definition in header. + pub fn header(header_name: N, scheme: S) -> Self + where + N: Into, + S: Into>, + { + let mut builder = Self::builder() + .location(HttpAuthLocation::Header) + .name(header_name); + let scheme: Option = scheme.into(); + if scheme.is_some() { + builder.set_scheme(scheme); + } + builder.build() + } + + /// Constructs a new HTTP auth definition following the RFC 2617 for Basic Auth. + pub fn basic_auth() -> Self { + Self::builder() + .location(HttpAuthLocation::Header) + .name("Authorization".to_owned()) + .scheme("Basic".to_owned()) + .build() + } + + /// Constructs a new HTTP auth definition following the RFC 2617 for Digest Auth. + pub fn digest_auth() -> Self { + Self::builder() + .location(HttpAuthLocation::Header) + .name("Authorization".to_owned()) + .scheme("Digest".to_owned()) + .build() + } + + /// Constructs a new HTTP auth definition following the RFC 6750 for Bearer Auth. + pub fn bearer_auth() -> Self { + Self::builder() + .location(HttpAuthLocation::Header) + .name("Authorization".to_owned()) + .scheme("Bearer".to_owned()) + .build() + } + + /// Constructs a new HTTP auth definition in query string. + pub fn query(name: impl Into) -> Self { + Self::builder() + .location(HttpAuthLocation::Query) + .name(name.into()) + .build() + } + + /// Returns the HTTP auth location. + pub fn location(&self) -> HttpAuthLocation { + self.location + } + + /// Returns the HTTP auth name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the HTTP auth scheme. + pub fn scheme(&self) -> Option<&str> { + self.scheme.as_deref() + } +} + +/// Types associated with [`HttpAuthDefinition`]. +pub mod http_auth_definition { + use super::HttpAuthDefinition; + use crate::{ + definition::HttpAuthLocation, + error::{AuthError, AuthErrorKind}, + }; + + /// A builder for [`HttpAuthDefinition`]. + #[derive(Debug, Default)] + pub struct Builder { + location: Option, + name: Option, + scheme: Option, + } + + impl Builder { + /// Sets the HTTP auth location. + pub fn location(mut self, location: HttpAuthLocation) -> Self { + self.location = Some(location); + self + } + + /// Sets the HTTP auth location. + pub fn set_location(&mut self, location: Option) -> &mut Self { + self.location = location; + self + } + + /// Sets the the HTTP auth name. + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + /// Sets the the HTTP auth name. + pub fn set_name(&mut self, name: Option) -> &mut Self { + self.name = name; + self + } + + /// Sets the HTTP auth scheme. + pub fn scheme(mut self, scheme: impl Into) -> Self { + self.scheme = Some(scheme.into()); + self + } + + /// Sets the HTTP auth scheme. + pub fn set_scheme(&mut self, scheme: Option) -> &mut Self { + self.scheme = scheme; + self + } + + /// Constructs a [`HttpAuthDefinition`] from the builder. + pub fn build(self) -> HttpAuthDefinition { + if self.scheme.is_some() + && self + .name + .as_deref() + .map_or("".to_string(), |s| s.to_ascii_lowercase()) + != "authorization" + { + // Stop execution because the Smithy model should not contain such combination. + // Otherwise, this would cause unexpected behavior in the SDK. + panic!("{}", AuthError::from(AuthErrorKind::SchemeNotAllowed)); + } + HttpAuthDefinition { + location: self.location.unwrap_or_else(|| { + panic!( + "{}", + AuthError::from(AuthErrorKind::MissingRequiredField("location")) + ) + }), + name: self.name.unwrap_or_else(|| { + panic!( + "{}", + AuthError::from(AuthErrorKind::MissingRequiredField("name")) + ) + }), + scheme: self.scheme, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::HttpAuthDefinition; + use crate::location::HttpAuthLocation; + + #[test] + fn definition_for_header_without_scheme() { + let definition = HttpAuthDefinition::header("Header", None); + assert_eq!(definition.location, HttpAuthLocation::Header); + assert_eq!(definition.name, "Header"); + assert_eq!(definition.scheme, None); + } + + #[test] + fn definition_for_authorization_header_with_scheme() { + let definition = HttpAuthDefinition::header("authorization", "Scheme".to_owned()); + assert_eq!(definition.location(), HttpAuthLocation::Header); + assert_eq!(definition.name(), "authorization"); + assert_eq!(definition.scheme(), Some("Scheme")); + } + + #[test] + #[should_panic] + fn definition_fails_with_scheme_not_allowed() { + let _ = HttpAuthDefinition::header("Invalid".to_owned(), "Scheme".to_owned()); + } + + #[test] + fn definition_for_basic() { + let definition = HttpAuthDefinition::basic_auth(); + assert_eq!( + definition, + HttpAuthDefinition { + location: HttpAuthLocation::Header, + name: "Authorization".to_owned(), + scheme: Some("Basic".to_owned()), + } + ); + } + + #[test] + fn definition_for_digest() { + let definition = HttpAuthDefinition::digest_auth(); + assert_eq!(definition.location(), HttpAuthLocation::Header); + assert_eq!(definition.name(), "Authorization"); + assert_eq!(definition.scheme(), Some("Digest")); + } + + #[test] + fn definition_for_bearer_token() { + let definition = HttpAuthDefinition::bearer_auth(); + assert_eq!(definition.location(), HttpAuthLocation::Header); + assert_eq!(definition.name(), "Authorization"); + assert_eq!(definition.scheme(), Some("Bearer")); + } + + #[test] + fn definition_for_query() { + let definition = HttpAuthDefinition::query("query_key"); + assert_eq!(definition.location(), HttpAuthLocation::Query); + assert_eq!(definition.name(), "query_key"); + assert_eq!(definition.scheme(), None); + } +} diff --git a/rust-runtime/aws-smithy-http-auth/src/error.rs b/rust-runtime/aws-smithy-http-auth/src/error.rs new file mode 100644 index 0000000000..227dbe1cf2 --- /dev/null +++ b/rust-runtime/aws-smithy-http-auth/src/error.rs @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! HTTP Auth Error + +use std::cmp::PartialEq; +use std::fmt::Debug; + +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum AuthErrorKind { + InvalidLocation, + MissingRequiredField(&'static str), + SchemeNotAllowed, +} + +/// Error for Smithy authentication +#[derive(Debug, Eq, PartialEq)] +pub struct AuthError { + kind: AuthErrorKind, +} + +impl std::fmt::Display for AuthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use AuthErrorKind::*; + match &self.kind { + InvalidLocation => write!(f, "invalid location: expected `header` or `query`"), + MissingRequiredField(field) => write!(f, "missing required field: {}", field), + SchemeNotAllowed => write!( + f, + "scheme only allowed when it is set into the `Authorization` header" + ), + } + } +} + +impl From for AuthError { + fn from(kind: AuthErrorKind) -> Self { + Self { kind } + } +} diff --git a/rust-runtime/aws-smithy-http-auth/src/lib.rs b/rust-runtime/aws-smithy-http-auth/src/lib.rs new file mode 100644 index 0000000000..ada202143e --- /dev/null +++ b/rust-runtime/aws-smithy-http-auth/src/lib.rs @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#![allow(clippy::derive_partial_eq_without_eq)] +#![warn(missing_debug_implementations, missing_docs, rustdoc::all)] + +//! Smithy HTTP Auth Types + +pub mod api_key; +pub mod definition; +pub mod error; +pub mod location; diff --git a/rust-runtime/aws-smithy-http-auth/src/location.rs b/rust-runtime/aws-smithy-http-auth/src/location.rs new file mode 100644 index 0000000000..9fc0ced0e8 --- /dev/null +++ b/rust-runtime/aws-smithy-http-auth/src/location.rs @@ -0,0 +1,73 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! HTTP Auth Location + +use std::cmp::PartialEq; +use std::fmt::Debug; + +use crate::error::{AuthError, AuthErrorKind}; + +/// Enum for describing where the HTTP Auth can be placed. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum HttpAuthLocation { + /// In the HTTP header. + #[default] + Header, + /// In the query string of the URL. + Query, +} + +impl HttpAuthLocation { + fn as_str(&self) -> &'static str { + match self { + Self::Header => "header", + Self::Query => "query", + } + } +} + +impl TryFrom<&str> for HttpAuthLocation { + type Error = AuthError; + fn try_from(value: &str) -> Result { + match value { + "header" => Ok(Self::Header), + "query" => Ok(Self::Query), + _ => Err(AuthError::from(AuthErrorKind::InvalidLocation)), + } + } +} + +impl TryFrom for HttpAuthLocation { + type Error = AuthError; + fn try_from(value: String) -> Result { + Self::try_from(value.as_str()) + } +} + +impl AsRef for HttpAuthLocation { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl std::fmt::Display for HttpAuthLocation { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Display::fmt(&self.as_str(), f) + } +} + +#[cfg(test)] +mod tests { + use super::HttpAuthLocation; + use crate::error::{AuthError, AuthErrorKind}; + + #[test] + fn fails_if_location_is_invalid() { + let actual = HttpAuthLocation::try_from("invalid").unwrap_err(); + let expected = AuthError::from(AuthErrorKind::InvalidLocation); + assert_eq!(actual, expected); + } +} diff --git a/rust-runtime/aws-smithy-http/src/lib.rs b/rust-runtime/aws-smithy-http/src/lib.rs index 92b8b737a7..f777e15c82 100644 --- a/rust-runtime/aws-smithy-http/src/lib.rs +++ b/rust-runtime/aws-smithy-http/src/lib.rs @@ -28,6 +28,8 @@ pub mod middleware; pub mod operation; pub mod property_bag; pub mod query; +#[doc(hidden)] +pub mod query_writer; pub mod response; pub mod result; pub mod retry; diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/query_writer.rs b/rust-runtime/aws-smithy-http/src/query_writer.rs similarity index 94% rename from aws/rust-runtime/aws-sigv4/src/http_request/query_writer.rs rename to rust-runtime/aws-smithy-http/src/query_writer.rs index 8ee4cdc55e..a7f9d7cfe1 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/query_writer.rs +++ b/rust-runtime/aws-smithy-http/src/query_writer.rs @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -use crate::http_request::url_escape::percent_encode_query; +use crate::query::fmt_string as percent_encode_query; use http::Uri; /// Utility for updating the query string in a [`Uri`]. -pub(super) struct QueryWriter { +pub struct QueryWriter { base_uri: Uri, new_path_and_query: String, prefix: Option, @@ -15,7 +15,7 @@ pub(super) struct QueryWriter { impl QueryWriter { /// Creates a new `QueryWriter` based off the given `uri`. - pub(super) fn new(uri: &Uri) -> Self { + pub fn new(uri: &Uri) -> Self { let new_path_and_query = uri .path_and_query() .map(|pq| pq.to_string()) @@ -35,7 +35,7 @@ impl QueryWriter { } /// Clears all query parameters. - pub(super) fn clear_params(&mut self) { + pub fn clear_params(&mut self) { if let Some(index) = self.new_path_and_query.find('?') { self.new_path_and_query.truncate(index); self.prefix = Some('?'); @@ -44,7 +44,7 @@ impl QueryWriter { /// Inserts a new query parameter. The key and value are percent encoded /// by `QueryWriter`. Passing in percent encoded values will result in double encoding. - pub(super) fn insert(&mut self, k: &str, v: &str) { + pub fn insert(&mut self, k: &str, v: &str) { if let Some(prefix) = self.prefix { self.new_path_and_query.push(prefix); } @@ -56,12 +56,12 @@ impl QueryWriter { } /// Returns just the built query string. - pub(super) fn build_query(self) -> String { + pub fn build_query(self) -> String { self.build_uri().query().unwrap_or_default().to_string() } /// Returns a full [`Uri`] with the query string updated. - pub(super) fn build_uri(self) -> Uri { + pub fn build_uri(self) -> Uri { let mut parts = self.base_uri.into_parts(); parts.path_and_query = Some( self.new_path_and_query