From d385601433829fa4e5d4348634a96091292c7f75 Mon Sep 17 00:00:00 2001 From: Simonas Kazlauskas Date: Mon, 15 Mar 2021 17:51:08 +0200 Subject: [PATCH] --all-features and --no-default-features This adjusts the code and documentation for `--all-features` and `--no-default-features` to work correctly. With `--no-default-features` no `DefaultAuthenticator` is made available. Users are in control of picking the `Connector` they want to use, and are not forced to stomach a dependency on `rustls` or `hyper-tls` if their TLS implementation of choice doesn't happen to match one of the two. To indicate this, the unstable `doc_cfg` feature is used to build documentation on docs.rs. That way the generated documentation has notices on these types that look as such: > This is supported on crate features hyper-rustls or hyper-tls only. Additionally this functionality is tested via additional coverage in the Actions' CI. --- .github/workflows/test.yml | 29 +++++++++++++++- Cargo.toml | 13 +++++++ src/authenticator.rs | 70 ++++++++++++++++++++++++++++++++++---- src/error.rs | 5 +-- src/installed.rs | 8 +++-- src/lib.rs | 2 ++ src/service_account.rs | 28 ++++++--------- tests/tests.rs | 26 ++++---------- 8 files changed, 132 insertions(+), 49 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 190164d..5ff8f53 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,17 +4,44 @@ name: Actions CI jobs: build_and_test: - name: yup-oauth2 + name: yup-oauth2 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable + profile: minimal + default: true - uses: actions-rs/cargo@v1 with: command: test + - uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features + - uses: actions-rs/cargo@v1 + with: + command: test + args: --no-default-features --tests --examples - uses: actions-rs/cargo@v1 with: command: build args: --examples + + doc: + name: yup-oauth2 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + profile: minimal + default: true + - uses: actions-rs/cargo@v1 + with: + command: doc + args: --all-features + env: + RUSTDOCFLAGS: --cfg yup_oauth2_docsrs diff --git a/Cargo.toml b/Cargo.toml index f9bf87d..89d7886 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,15 @@ keywords = ["google", "oauth", "v2"] license = "MIT OR Apache-2.0" edition = "2018" +[[example]] +name = "custom_flow" +required-features = ["hyper-rustls"] + +[[test]] +name = "tests" +required-features = ["hyper-rustls"] + + [features] default = ["hyper-rustls"] @@ -41,3 +50,7 @@ webbrowser = "0.5" [workspace] members = ["examples/test-installed/", "examples/test-svc-acct/", "examples/test-device/", "examples/service_account", "examples/drive_example"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "yup_oauth2_docsrs"] diff --git a/src/authenticator.rs b/src/authenticator.rs index 7452213..4da0fdb 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -132,6 +132,11 @@ where } } +enum StorageType { + Memory, + Disk(PathBuf), +} + /// Configure an Authenticator using the builder pattern. pub struct AuthenticatorBuilder { hyper_client_builder: C, @@ -157,6 +162,11 @@ pub struct AuthenticatorBuilder { pub struct InstalledFlowAuthenticator; impl InstalledFlowAuthenticator { /// Use the builder pattern to create an Authenticator that uses the installed flow. + #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] + #[cfg_attr( + yup_oauth2_docsrs, + doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) + )] pub fn builder( app_secret: ApplicationSecret, method: InstalledFlowReturnMethod, @@ -180,6 +190,11 @@ impl InstalledFlowAuthenticator { pub struct DeviceFlowAuthenticator; impl DeviceFlowAuthenticator { /// Use the builder pattern to create an Authenticator that uses the device flow. + #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] + #[cfg_attr( + yup_oauth2_docsrs, + doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) + )] pub fn builder( app_secret: ApplicationSecret, ) -> AuthenticatorBuilder { @@ -200,6 +215,11 @@ impl DeviceFlowAuthenticator { pub struct ServiceAccountAuthenticator; impl ServiceAccountAuthenticator { /// Use the builder pattern to create an Authenticator that uses a service account. + #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] + #[cfg_attr( + yup_oauth2_docsrs, + doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) + )] pub fn builder( service_account_key: ServiceAccountKey, ) -> AuthenticatorBuilder { @@ -250,6 +270,11 @@ impl AuthenticatorBuilder { }) } + #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] + #[cfg_attr( + yup_oauth2_docsrs, + doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) + )] fn with_auth_flow(auth_flow: F) -> AuthenticatorBuilder { AuthenticatorBuilder { hyper_client_builder: DefaultHyperClient, @@ -484,27 +509,58 @@ pub trait HyperClientBuilder { fn build_hyper_client(self) -> hyper::Client; } -#[cfg(not(feature = "hyper-tls"))] +impl HyperClientBuilder for hyper::Client +where + C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, +{ + type Connector = C; + + fn build_hyper_client(self) -> hyper::Client { + self + } +} + +#[cfg(feature = "hyper-rustls")] +#[cfg_attr( + yup_oauth2_docsrs, + doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) +)] /// Default authenticator type pub type DefaultAuthenticator = Authenticator>; -#[cfg(feature = "hyper-tls")] + +#[cfg(all(not(feature = "hyper-rustls"), feature = "hyper-tls"))] +#[cfg_attr( + yup_oauth2_docsrs, + doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) +)] /// Default authenticator type pub type DefaultAuthenticator = Authenticator>; /// The builder value used when the default hyper client should be used. +#[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] +#[cfg_attr( + yup_oauth2_docsrs, + doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) +)] pub struct DefaultHyperClient; + +#[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] +#[cfg_attr( + yup_oauth2_docsrs, + doc(cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))) +)] impl HyperClientBuilder for DefaultHyperClient { - #[cfg(not(feature = "hyper-tls"))] + #[cfg(feature = "hyper-rustls")] type Connector = hyper_rustls::HttpsConnector; - #[cfg(feature = "hyper-tls")] + #[cfg(all(not(feature = "hyper-rustls"), feature = "hyper-tls"))] type Connector = hyper_tls::HttpsConnector; fn build_hyper_client(self) -> hyper::Client { - #[cfg(not(feature = "hyper-tls"))] + #[cfg(feature = "hyper-rustls")] let connector = hyper_rustls::HttpsConnector::with_native_roots(); - #[cfg(feature = "hyper-tls")] + #[cfg(all(not(feature = "hyper-rustls"), feature = "hyper-tls"))] let connector = hyper_tls::HttpsConnector::new(); hyper::Client::builder() @@ -537,8 +593,8 @@ enum StorageType { #[cfg(test)] mod tests { use super::*; - #[test] + #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] fn ensure_send_sync() { fn is_send_sync() {} is_send_sync::::Connector>>() diff --git a/src/error.rs b/src/error.rs index 04e9210..d43073b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,8 +8,9 @@ use std::io; use serde::Deserialize; /// Error returned by the authorization server. -/// https://tools.ietf.org/html/rfc6749#section-5.2 -/// https://tools.ietf.org/html/rfc8628#section-3.5 +/// +/// +/// #[derive(Deserialize, Debug, PartialEq, Eq)] pub struct AuthError { /// Error code from the server. diff --git a/src/installed.rs b/src/installed.rs index 402ff61..2336d47 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -60,7 +60,9 @@ where }) } -/// cf. https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi +/// Method by which the user agent return token to this application. +/// +/// cf. pub enum InstalledFlowReturnMethod { /// Involves showing a URL to the user and asking to copy a code from their browser /// (default) @@ -71,8 +73,8 @@ pub enum InstalledFlowReturnMethod { } /// InstalledFlowImpl provides tokens for services that follow the "Installed" OAuth flow. (See -/// https://www.oauth.com/oauth2-servers/authorization/, -/// https://developers.google.com/identity/protocols/OAuth2InstalledApp). +/// , +/// ). pub struct InstalledFlow { pub(crate) app_secret: ApplicationSecret, pub(crate) method: InstalledFlowReturnMethod, diff --git a/src/lib.rs b/src/lib.rs index 284903e..cf9a7ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,6 +69,8 @@ //! ``` //! #![deny(missing_docs)] +#![cfg_attr(yup_oauth2_docsrs, feature(doc_cfg))] + pub mod authenticator; pub mod authenticator_delegate; mod device; diff --git a/src/service_account.rs b/src/service_account.rs index 3e1f718..4dbf8ad 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -1,7 +1,8 @@ -//! This module provides a token source (`GetToken`) that obtains tokens for service accounts. +//! This module provides a flow that obtains tokens for service accounts. +//! //! Service accounts are usually used by software (i.e., non-human actors) to get access to -//! resources. Currently, this module only works with RS256 JWTs, which makes it at least suitable for -//! authentication with Google services. +//! resources. Currently, this module only works with RS256 JWTs, which makes it at least suitable +//! for authentication with Google services. //! //! Resources: //! - [Using OAuth 2.0 for Server to Server @@ -9,7 +10,6 @@ //! - [JSON Web Tokens](https://jwt.io/) //! //! Copyright (c) 2016 Google Inc (lewinb@google.com). -//! use crate::error::Error; use crate::types::TokenInfo; @@ -54,8 +54,9 @@ fn decode_rsa_key(pem_pkcs8: &str) -> Result { } } -/// JSON schema of secret service account key. You can obtain the key from -/// the Cloud Console at https://console.cloud.google.com/. +/// JSON schema of secret service account key. +/// +/// You can obtain the key from the [Cloud Console](https://console.cloud.google.com/). /// /// You can use `helpers::read_service_account_key()` as a quick way to read a JSON client /// secret into a ServiceAccountKey. @@ -210,12 +211,10 @@ impl ServiceAccountFlow { #[cfg(test)] mod tests { + use super::*; + use crate::authenticator::HyperClientBuilder; use crate::helper::read_service_account_key; - #[cfg(not(feature = "hyper-tls"))] - use hyper_rustls::HttpsConnector; - #[cfg(feature = "hyper-tls")] - use hyper_tls::HttpsConnector; // Valid but deactivated key. const TEST_PRIVATE_KEY_PATH: &'static str = "examples/Sanguine-69411a0c0eea.json"; @@ -223,18 +222,13 @@ mod tests { // Uncomment this test to verify that we can successfully obtain tokens. //#[tokio::test] #[allow(dead_code)] + #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))] async fn test_service_account_e2e() { let key = read_service_account_key(TEST_PRIVATE_KEY_PATH) .await .unwrap(); let acc = ServiceAccountFlow::new(ServiceAccountFlowOpts { key, subject: None }).unwrap(); - #[cfg(not(feature = "hyper-tls"))] - let https = HttpsConnector::with_native_roots(); - #[cfg(feature = "hyper-tls")] - let https = HttpsConnector::new(); - let client = hyper::Client::builder() - .pool_max_idle_per_host(0) - .build::<_, hyper::Body>(https); + let client = crate::authenticator::DefaultHyperClient.build_hyper_client(); println!( "{:?}", acc.token(&client, &["https://www.googleapis.com/auth/pubsub"]) diff --git a/tests/tests.rs b/tests/tests.rs index 183ed77..75f0ab5 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,5 +1,5 @@ use yup_oauth2::{ - authenticator::Authenticator, + authenticator::{DefaultAuthenticator, DefaultHyperClient, HyperClientBuilder}, authenticator_delegate::{DeviceAuthResponse, DeviceFlowDelegate, InstalledFlowDelegate}, error::{AuthError, AuthErrorCode}, ApplicationSecret, DeviceFlowAuthenticator, Error, InstalledFlowAuthenticator, @@ -11,12 +11,7 @@ use std::path::PathBuf; use std::pin::Pin; use httptest::{matchers::*, responders::json_encoded, Expectation, Server}; -use hyper::client::connect::HttpConnector; use hyper::Uri; -#[cfg(not(feature = "hyper-tls"))] -use hyper_rustls::HttpsConnector; -#[cfg(feature = "hyper-tls")] -use hyper_tls::HttpsConnector; use url::form_urlencoded; /// Utility function for parsing json. Useful in unit tests. Simply wrap the @@ -27,7 +22,7 @@ macro_rules! parse_json { } } -async fn create_device_flow_auth(server: &Server) -> Authenticator> { +async fn create_device_flow_auth(server: &Server) -> DefaultAuthenticator { let app_secret: ApplicationSecret = parse_json!({ "client_id": "902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com", "project_id": "yup-test-243420", @@ -166,7 +161,7 @@ async fn create_installed_flow_auth( server: &Server, method: InstalledFlowReturnMethod, filename: Option, -) -> Authenticator> { +) -> DefaultAuthenticator { let app_secret: ApplicationSecret = parse_json!({ "client_id": "902216714886-k2v9uei3p1dk6h686jbsn9mo96tnbvto.apps.googleusercontent.com", "project_id": "yup-test-243420", @@ -176,7 +171,7 @@ async fn create_installed_flow_auth( "client_secret": "iuMPN6Ne1PD7cos29Tk9rlqH", "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob","http://localhost"], }); - struct FD(hyper::Client>); + struct FD(hyper::Client<::Connector>); impl InstalledFlowDelegate for FD { /// Depending on need_code, return the pre-set code or send the code to the server at /// the redirect_uri given in the url. @@ -219,13 +214,8 @@ async fn create_installed_flow_auth( } } - let mut builder = - InstalledFlowAuthenticator::builder(app_secret, method).flow_delegate(Box::new(FD( - #[cfg(not(feature = "hyper-tls"))] - hyper::Client::builder().build(HttpsConnector::with_native_roots()), - #[cfg(feature = "hyper-tls")] - hyper::Client::builder().build(HttpsConnector::new()), - ))); + let mut builder = InstalledFlowAuthenticator::builder(app_secret, method) + .flow_delegate(Box::new(FD(DefaultHyperClient.build_hyper_client()))); builder = if let Some(filename) = filename { builder.persist_tokens_to_disk(filename) @@ -321,9 +311,7 @@ async fn test_installed_error() { assert!(format!("{}", tokr.unwrap_err()).contains("invalid_code")); } -async fn create_service_account_auth( - server: &Server, -) -> Authenticator> { +async fn create_service_account_auth(server: &Server) -> DefaultAuthenticator { let key: ServiceAccountKey = parse_json!({ "type": "service_account", "project_id": "yup-test-243420",