From 5256f642d78ab286ed2e4b2c83cbfc52b4b8d8e4 Mon Sep 17 00:00:00 2001 From: Glenn Griffin Date: Wed, 20 Nov 2019 14:01:17 -0800 Subject: [PATCH] Tie ServiceAccount's into Authenticator. Prior to this change DeviceFlow and InstalledFlow were used within Authenticator, while ServiceAccountAccess was used on it's own. AFAICT this was the case because ServiceAccountAccess never used refresh tokens and Authenticator assumed all tokens contained refresh tokens. Authenticator was recently modified to handle the case where a token does not contain a refresh token so I don't see any reason to keep the service account access separate anymore. Folding it into the authenticator provides a nice consistent interface, and the service account implementation no longer needs to provide it's own caching since it is now handled by Authenticator. --- examples/test-svc-acct/src/main.rs | 2 +- src/authenticator.rs | 193 +++++++++++++++++++--------- src/device.rs | 35 +++--- src/installed.rs | 20 +-- src/lib.rs | 9 +- src/service_account.rs | 195 ++++++++--------------------- src/storage.rs | 29 ++--- 7 files changed, 233 insertions(+), 250 deletions(-) diff --git a/examples/test-svc-acct/src/main.rs b/examples/test-svc-acct/src/main.rs index 8ff8db0..1b73a69 100644 --- a/examples/test-svc-acct/src/main.rs +++ b/examples/test-svc-acct/src/main.rs @@ -4,7 +4,7 @@ use yup_oauth2::ServiceAccountAuthenticator; #[tokio::main] async fn main() { let creds = yup_oauth2::service_account_key_from_file("serviceaccount.json").unwrap(); - let sa = ServiceAccountAuthenticator::builder(creds).build().unwrap(); + let sa = ServiceAccountAuthenticator::builder(creds).build().await.unwrap(); let scopes = &["https://www.googleapis.com/auth/pubsub"]; let tok = sa.token(scopes).await.unwrap(); diff --git a/src/authenticator.rs b/src/authenticator.rs index 1402448..b74a3c8 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -6,6 +6,7 @@ use crate::device::DeviceFlow; use crate::error::Error; use crate::installed::{InstalledFlow, InstalledFlowReturnMethod}; use crate::refresh::RefreshFlow; +use crate::service_account::{ServiceAccountFlow, ServiceAccountFlowOpts, ServiceAccountKey}; use crate::storage::{self, Storage}; use crate::types::{ApplicationSecret, Token}; use private::AuthFlow; @@ -20,7 +21,6 @@ use std::time::Duration; /// and optionally persisting tokens to disk. pub struct Authenticator { hyper_client: hyper::Client, - app_secret: ApplicationSecret, auth_delegate: Box, storage: Storage, auth_flow: AuthFlow, @@ -36,19 +36,22 @@ where T: AsRef, { let hashed_scopes = storage::ScopesAndFilter::from(scopes); - match self.storage.get(hashed_scopes) { - Some(t) if !t.expired() => { + match (self.storage.get(hashed_scopes), self.auth_flow.app_secret()) { + (Some(t), _) if !t.expired() => { // unexpired token found Ok(t) } - Some(Token { - refresh_token: Some(refresh_token), - .. - }) => { + ( + Some(Token { + refresh_token: Some(refresh_token), + .. + }), + Some(app_secret), + ) => { // token is expired but has a refresh token. let token = match RefreshFlow::refresh_token( &self.hyper_client, - &self.app_secret, + app_secret, &refresh_token, ) .await @@ -62,16 +65,9 @@ where self.storage.set(hashed_scopes, token.clone()).await; Ok(token) } - None - | Some(Token { - refresh_token: None, - .. - }) => { - // no token in the cache or the token returned does not contain a refresh token. - let t = self - .auth_flow - .token(&self.hyper_client, &self.app_secret, scopes) - .await?; + _ => { + // no token in the cache or the token returned can't be refreshed. + let t = self.auth_flow.token(&self.hyper_client, scopes).await?; self.storage.set(hashed_scopes, t.clone()).await; Ok(t) } @@ -82,7 +78,6 @@ where /// Configure an Authenticator using the builder pattern. pub struct AuthenticatorBuilder { hyper_client_builder: C, - app_secret: ApplicationSecret, auth_delegate: Box, storage_type: StorageType, auth_flow: F, @@ -110,10 +105,9 @@ impl InstalledFlowAuthenticator { app_secret: ApplicationSecret, method: InstalledFlowReturnMethod, ) -> AuthenticatorBuilder { - AuthenticatorBuilder::::with_auth_flow( - app_secret, - InstalledFlow::new(method), - ) + AuthenticatorBuilder::::with_auth_flow(InstalledFlow::new( + app_secret, method, + )) } } @@ -133,7 +127,30 @@ impl DeviceFlowAuthenticator { pub fn builder( app_secret: ApplicationSecret, ) -> AuthenticatorBuilder { - AuthenticatorBuilder::::with_auth_flow(app_secret, DeviceFlow::new()) + AuthenticatorBuilder::::with_auth_flow(DeviceFlow::new(app_secret)) + } +} + +/// Create an authenticator that uses a service account. +/// ``` +/// # async fn foo() { +/// # let service_account_key = yup_oauth2::service_account_key_from_file("/tmp/foo").unwrap(); +/// let authenticator = yup_oauth2::ServiceAccountAuthenticator::builder(service_account_key) +/// .build() +/// .await +/// .expect("failed to create authenticator"); +/// # } +/// ``` +pub struct ServiceAccountAuthenticator; +impl ServiceAccountAuthenticator { + /// Use the builder pattern to create an Authenticator that uses a service account. + pub fn builder( + service_account_key: ServiceAccountKey, + ) -> AuthenticatorBuilder { + AuthenticatorBuilder::::with_auth_flow(ServiceAccountFlowOpts { + key: service_account_key, + subject: None, + }) } } @@ -153,14 +170,17 @@ impl DeviceFlowAuthenticator { /// # } /// ``` impl AuthenticatorBuilder { - /// Create the authenticator. - pub async fn build(self) -> io::Result> + async fn common_build( + hyper_client_builder: C, + storage_type: StorageType, + auth_delegate: Box, + auth_flow: AuthFlow, + ) -> io::Result> where C: HyperClientBuilder, - F: Into, { - let hyper_client = self.hyper_client_builder.build_hyper_client(); - let storage = match self.storage_type { + let hyper_client = hyper_client_builder.build_hyper_client(); + let storage = match storage_type { StorageType::Memory => Storage::Memory { tokens: Mutex::new(storage::JSONTokens::new()), }, @@ -169,20 +189,15 @@ impl AuthenticatorBuilder { Ok(Authenticator { hyper_client, - app_secret: self.app_secret, storage, - auth_delegate: self.auth_delegate, - auth_flow: self.auth_flow.into(), + auth_delegate, + auth_flow, }) } - fn with_auth_flow( - app_secret: ApplicationSecret, - auth_flow: F, - ) -> AuthenticatorBuilder { + fn with_auth_flow(auth_flow: F) -> AuthenticatorBuilder { AuthenticatorBuilder { hyper_client_builder: DefaultHyperClient, - app_secret, auth_delegate: Box::new(DefaultAuthenticatorDelegate), storage_type: StorageType::Memory, auth_flow, @@ -196,7 +211,6 @@ impl AuthenticatorBuilder { ) -> AuthenticatorBuilder, F> { AuthenticatorBuilder { hyper_client_builder: hyper_client, - app_secret: self.app_secret, auth_delegate: self.auth_delegate, storage_type: self.storage_type, auth_flow: self.auth_flow, @@ -282,6 +296,20 @@ impl AuthenticatorBuilder { ..self } } + + /// Create the authenticator. + pub async fn build(self) -> io::Result> + where + C: HyperClientBuilder, + { + Self::common_build( + self.hyper_client_builder, + self.storage_type, + self.auth_delegate, + AuthFlow::DeviceFlow(self.auth_flow), + ) + .await + } } /// ## Methods available when building an installed flow Authenticator. @@ -311,36 +339,88 @@ impl AuthenticatorBuilder { ..self } } + + /// Create the authenticator. + pub async fn build(self) -> io::Result> + where + C: HyperClientBuilder, + { + Self::common_build( + self.hyper_client_builder, + self.storage_type, + self.auth_delegate, + AuthFlow::InstalledFlow(self.auth_flow), + ) + .await + } +} + +/// ## Methods available when building a service account authenticator. +/// ``` +/// # async fn foo() { +/// # let service_account_key = yup_oauth2::service_account_key_from_file("/tmp/foo").unwrap(); +/// let authenticator = yup_oauth2::ServiceAccountAuthenticator::builder( +/// service_account_key, +/// ) +/// .subject("mysubject") +/// .build() +/// .await +/// .expect("failed to create authenticator"); +/// # } +/// ``` +impl AuthenticatorBuilder { + /// Use the provided subject. + pub fn subject(self, subject: impl Into) -> Self { + AuthenticatorBuilder { + auth_flow: ServiceAccountFlowOpts { + subject: Some(subject.into()), + ..self.auth_flow + }, + ..self + } + } + + /// Create the authenticator. + pub async fn build(self) -> io::Result> + where + C: HyperClientBuilder, + { + let service_account_auth_flow = ServiceAccountFlow::new(self.auth_flow)?; + Self::common_build( + self.hyper_client_builder, + self.storage_type, + self.auth_delegate, + AuthFlow::ServiceAccountFlow(service_account_auth_flow), + ) + .await + } } mod private { use crate::device::DeviceFlow; use crate::error::Error; use crate::installed::InstalledFlow; + use crate::service_account::ServiceAccountFlow; use crate::types::{ApplicationSecret, Token}; pub enum AuthFlow { DeviceFlow(DeviceFlow), InstalledFlow(InstalledFlow), - } - - impl From for AuthFlow { - fn from(device_flow: DeviceFlow) -> AuthFlow { - AuthFlow::DeviceFlow(device_flow) - } - } - - impl From for AuthFlow { - fn from(installed_flow: InstalledFlow) -> AuthFlow { - AuthFlow::InstalledFlow(installed_flow) - } + ServiceAccountFlow(ServiceAccountFlow), } impl AuthFlow { + pub(crate) fn app_secret(&self) -> Option<&ApplicationSecret> { + match self { + AuthFlow::DeviceFlow(device_flow) => Some(&device_flow.app_secret), + AuthFlow::InstalledFlow(installed_flow) => Some(&installed_flow.app_secret), + AuthFlow::ServiceAccountFlow(_) => None, + } + } + pub(crate) async fn token<'a, C, T>( &'a self, hyper_client: &'a hyper::Client, - app_secret: &'a ApplicationSecret, scopes: &'a [T], ) -> Result where @@ -348,18 +428,19 @@ mod private { C: hyper::client::connect::Connect + 'static, { match self { - AuthFlow::DeviceFlow(device_flow) => { - device_flow.token(hyper_client, app_secret, scopes).await - } + AuthFlow::DeviceFlow(device_flow) => device_flow.token(hyper_client, scopes).await, AuthFlow::InstalledFlow(installed_flow) => { - installed_flow.token(hyper_client, app_secret, scopes).await + installed_flow.token(hyper_client, scopes).await + } + AuthFlow::ServiceAccountFlow(service_account_flow) => { + service_account_flow.token(hyper_client, scopes).await } } } } } -/// A trait implemented for any hyper::Client as well as teh DefaultHyperClient. +/// A trait implemented for any hyper::Client as well as the DefaultHyperClient. pub trait HyperClientBuilder { /// The hyper connector that the resulting hyper client will use. type Connector: hyper::client::connect::Connect + 'static; diff --git a/src/device.rs b/src/device.rs index 6815963..d2758a6 100644 --- a/src/device.rs +++ b/src/device.rs @@ -22,6 +22,7 @@ pub const GOOGLE_GRANT_TYPE: &str = "http://oauth.net/grant_type/device/1.0"; /// * obtain a code to show to the user // * (repeatedly) poll for the user to authenticate your application pub struct DeviceFlow { + pub(crate) app_secret: ApplicationSecret, pub(crate) device_code_url: Cow<'static, str>, pub(crate) flow_delegate: Box, pub(crate) wait_duration: Duration, @@ -31,8 +32,9 @@ pub struct DeviceFlow { impl DeviceFlow { /// Create a new DeviceFlow. The default FlowDelegate will be used and the /// default wait time is 120 seconds. - pub(crate) fn new() -> Self { + pub(crate) fn new(app_secret: ApplicationSecret) -> Self { DeviceFlow { + app_secret, device_code_url: GOOGLE_DEVICE_CODE_URL.into(), flow_delegate: Box::new(DefaultFlowDelegate), wait_duration: Duration::from_secs(120), @@ -43,20 +45,24 @@ impl DeviceFlow { pub(crate) async fn token( &self, hyper_client: &hyper::Client, - app_secret: &ApplicationSecret, scopes: &[T], ) -> Result where T: AsRef, C: hyper::client::connect::Connect + 'static, { - let (pollinf, device_code) = - Self::request_code(app_secret, hyper_client, &self.device_code_url, scopes).await?; + let (pollinf, device_code) = Self::request_code( + &self.app_secret, + hyper_client, + &self.device_code_url, + scopes, + ) + .await?; self.flow_delegate.present_user_code(&pollinf); tokio::timer::Timeout::new( self.wait_for_device_token( hyper_client, - app_secret, + &self.app_secret, &pollinf, &device_code, &self.grant_type, @@ -296,6 +302,7 @@ mod tests { .build::<_, hyper::Body>(https); let flow = DeviceFlow { + app_secret, device_code_url: device_code_url.into(), flow_delegate: Box::new(FD), wait_duration: Duration::from_secs(5), @@ -333,11 +340,7 @@ mod tests { .create(); let token = flow - .token( - &client, - &app_secret, - &["https://www.googleapis.com/scope/1"], - ) + .token(&client, &["https://www.googleapis.com/scope/1"]) .await .expect("token failed"); assert_eq!("accesstoken", token.access_token); @@ -373,11 +376,7 @@ mod tests { .create(); let res = flow - .token( - &client, - &app_secret, - &["https://www.googleapis.com/scope/1"], - ) + .token(&client, &["https://www.googleapis.com/scope/1"]) .await; assert!(res.is_err()); assert!(format!("{}", res.unwrap_err()).contains("invalid_client_id")); @@ -411,11 +410,7 @@ mod tests { .create(); let res = flow - .token( - &client, - &app_secret, - &["https://www.googleapis.com/scope/1"], - ) + .token(&client, &["https://www.googleapis.com/scope/1"]) .await; assert!(res.is_err()); assert!(format!("{}", res.unwrap_err()).contains("Access denied by user")); diff --git a/src/installed.rs b/src/installed.rs index d0805d0..81528f9 100644 --- a/src/installed.rs +++ b/src/installed.rs @@ -66,14 +66,19 @@ pub enum InstalledFlowReturnMethod { /// 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, pub(crate) flow_delegate: Box, } impl InstalledFlow { /// Create a new InstalledFlow with the provided secret and method. - pub(crate) fn new(method: InstalledFlowReturnMethod) -> InstalledFlow { + pub(crate) fn new( + app_secret: ApplicationSecret, + method: InstalledFlowReturnMethod, + ) -> InstalledFlow { InstalledFlow { + app_secret, method, flow_delegate: Box::new(DefaultFlowDelegate), } @@ -88,7 +93,6 @@ impl InstalledFlow { pub(crate) async fn token( &self, hyper_client: &hyper::Client, - app_secret: &ApplicationSecret, scopes: &[T], ) -> Result where @@ -97,11 +101,11 @@ impl InstalledFlow { { match self.method { InstalledFlowReturnMethod::HTTPRedirect => { - self.ask_auth_code_via_http(hyper_client, app_secret, scopes) + self.ask_auth_code_via_http(hyper_client, &self.app_secret, scopes) .await } InstalledFlowReturnMethod::Interactive => { - self.ask_auth_code_interactively(hyper_client, app_secret, scopes) + self.ask_auth_code_interactively(hyper_client, &self.app_secret, scopes) .await } } @@ -470,6 +474,7 @@ mod tests { let fd = FD("authorizationcode".to_string(), client.clone()); let inf = InstalledFlow { + app_secret: app_secret.clone(), method: InstalledFlowReturnMethod::Interactive, flow_delegate: Box::new(fd), }; @@ -493,7 +498,7 @@ mod tests { .create(); let tok = inf - .token(&client, &app_secret, &["https://googleapis.com/some/scope"]) + .token(&client, &["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); assert_eq!("accesstoken", tok.access_token); @@ -505,6 +510,7 @@ mod tests { // Successful path with HTTP redirect. { let inf = InstalledFlow { + app_secret: app_secret.clone(), method: InstalledFlowReturnMethod::HTTPRedirect, flow_delegate: Box::new(FD( "authorizationcodefromlocalserver".to_string(), @@ -528,7 +534,7 @@ mod tests { .create(); let tok = inf - .token(&client, &app_secret, &["https://googleapis.com/some/scope"]) + .token(&client, &["https://googleapis.com/some/scope"]) .await .expect("failed to get token"); assert_eq!("accesstoken", tok.access_token); @@ -549,7 +555,7 @@ mod tests { .create(); let tokr = inf - .token(&client, &app_secret, &["https://googleapis.com/some/scope"]) + .token(&client, &["https://googleapis.com/some/scope"]) .await; assert!(tokr.is_err()); assert!(format!("{}", tokr.unwrap_err()).contains("invalid_code")); diff --git a/src/lib.rs b/src/lib.rs index 824eb9f..81520f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,18 +76,19 @@ pub mod error; mod helper; mod installed; mod refresh; -pub mod service_account; +mod service_account; mod storage; mod types; #[doc(inline)] -pub use crate::authenticator::{DeviceFlowAuthenticator, InstalledFlowAuthenticator}; +pub use crate::authenticator::{ + DeviceFlowAuthenticator, InstalledFlowAuthenticator, ServiceAccountAuthenticator, +}; pub use crate::helper::*; pub use crate::installed::InstalledFlowReturnMethod; -#[doc(inline)] -pub use crate::service_account::{ServiceAccountAuthenticator, ServiceAccountKey}; +pub use crate::service_account::ServiceAccountKey; #[doc(inline)] pub use crate::error::Error; diff --git a/src/service_account.rs b/src/service_account.rs index 211357d..abb4eda 100644 --- a/src/service_account.rs +++ b/src/service_account.rs @@ -11,13 +11,10 @@ //! Copyright (c) 2016 Google Inc (lewinb@google.com). //! -use crate::authenticator::{DefaultHyperClient, HyperClientBuilder}; use crate::error::{Error, JsonErrorOr}; -use crate::storage::{self, Storage}; use crate::types::Token; use std::io; -use std::sync::Mutex; use futures::prelude::*; use hyper::header; @@ -124,7 +121,7 @@ impl<'a> Claims<'a> { } /// A JSON Web Token ready for signing. -struct JWTSigner { +pub(crate) struct JWTSigner { signer: Box, } @@ -160,139 +157,40 @@ impl JWTSigner { } } -/// Create an authenticator that uses a service account. -/// ``` -/// # async fn foo() { -/// # let service_key = yup_oauth2::service_account_key_from_file("/tmp/foo").unwrap(); -/// let authenticator = yup_oauth2::ServiceAccountAuthenticator::builder(service_key) -/// .build() -/// .expect("failed to create authenticator"); -/// # } -/// ``` -pub struct ServiceAccountAuthenticator; -impl ServiceAccountAuthenticator { - /// Use the builder pattern to create an authenticator that uses a service - /// account. - pub fn builder(key: ServiceAccountKey) -> Builder { - Builder { - client: DefaultHyperClient, - key, - subject: None, - } - } +pub struct ServiceAccountFlowOpts { + pub(crate) key: ServiceAccountKey, + pub(crate) subject: Option, } -/// Configure a service account authenticator using the builder pattern. -pub struct Builder { - client: C, +/// ServiceAccountFlow can fetch oauth tokens using a service account. +pub struct ServiceAccountFlow { key: ServiceAccountKey, subject: Option, -} - -/// Methods available when building a service account authenticator. -/// ``` -/// # async fn foo() { -/// # let custom_hyper_client = hyper::Client::new(); -/// # let service_key = yup_oauth2::service_account_key_from_file("/tmp/foo").unwrap(); -/// let authenticator = yup_oauth2::ServiceAccountAuthenticator::builder(service_key) -/// .hyper_client(custom_hyper_client) -/// .subject("foo") -/// .build() -/// .expect("failed to create authenticator"); -/// # } -/// ``` -impl Builder { - /// Use the provided hyper client. - pub fn hyper_client(self, hyper_client: NewC) -> Builder { - Builder { - client: hyper_client, - key: self.key, - subject: self.subject, - } - } - - /// Use the provided subject. - pub fn subject(self, subject: impl Into) -> Self { - Builder { - subject: Some(subject.into()), - ..self - } - } - - /// Build the configured ServiceAccountAccess. - pub fn build(self) -> Result, io::Error> - where - C: HyperClientBuilder, - { - ServiceAccountAccess::new(self.client.build_hyper_client(), self.key, self.subject) - } -} - -/// ServiceAccountAccess can fetch oauth tokens using a service account. -pub struct ServiceAccountAccess { - client: hyper::Client, - key: ServiceAccountKey, - cache: Storage, - subject: Option, signer: JWTSigner, } -impl ServiceAccountAccess -where - C: hyper::client::connect::Connect + 'static, -{ - fn new( - client: hyper::Client, - key: ServiceAccountKey, - subject: Option, - ) -> Result { - let signer = JWTSigner::new(&key.private_key)?; - Ok(ServiceAccountAccess { - client, - key, - cache: Storage::Memory { - tokens: Mutex::new(storage::JSONTokens::new()), - }, - subject, +impl ServiceAccountFlow { + pub(crate) fn new(opts: ServiceAccountFlowOpts) -> Result { + let signer = JWTSigner::new(&opts.key.private_key)?; + Ok(ServiceAccountFlow { + key: opts.key, + subject: opts.subject, signer, }) } - /// Return the current token for the provided scopes. - pub async fn token(&self, scopes: &[T]) -> Result - where - T: AsRef, - { - let hashed_scopes = storage::ScopesAndFilter::from(scopes); - let cache = &self.cache; - match cache.get(hashed_scopes) { - Some(token) if !token.expired() => return Ok(token), - _ => {} - } - let token = Self::request_token( - &self.client, - &self.signer, - self.subject.as_ref().map(|x| x.as_str()), - &self.key, - scopes, - ) - .await?; - cache.set(hashed_scopes, token.clone()).await; - Ok(token) - } /// Send a request for a new Bearer token to the OAuth provider. - async fn request_token( - client: &hyper::client::Client, - signer: &JWTSigner, - subject: Option<&str>, - key: &ServiceAccountKey, + pub(crate) async fn token( + &self, + hyper_client: &hyper::Client, scopes: &[T], ) -> Result where T: AsRef, + C: hyper::client::connect::Connect + 'static, { - let claims = Claims::new(key, scopes, subject); - let signed = signer.sign_claims(&claims).map_err(|_| { + let claims = Claims::new(&self.key, scopes, self.subject.as_ref().map(|x| x.as_str())); + let signed = self.signer.sign_claims(&claims).map_err(|_| { Error::LowLevelError(io::Error::new( io::ErrorKind::Other, "unable to sign claims", @@ -301,11 +199,14 @@ where let rqbody = form_urlencoded::Serializer::new(String::new()) .extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", signed.as_str())]) .finish(); - let request = hyper::Request::post(&key.token_uri) + let request = hyper::Request::post(&self.key.token_uri) .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") .body(hyper::Body::from(rqbody)) .unwrap(); - let response = client.request(request).await.map_err(Error::ClientError)?; + let response = hyper_client + .request(request) + .await + .map_err(Error::ClientError)?; let body = response .into_body() .try_concat() @@ -349,12 +250,17 @@ mod tests { use super::*; use crate::helper::service_account_key_from_file; use crate::parse_json; + use hyper_rustls::HttpsConnector; use mockito::mock; #[tokio::test] async fn test_mocked_http() { env_logger::try_init().unwrap(); + let https = HttpsConnector::new(); + let client = hyper::Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(https); let server_url = &mockito::server_url(); let key: ServiceAccountKey = parse_json!({ "type": "service_account", @@ -387,25 +293,13 @@ mod tests { .with_body(json_response.to_string()) .expect(1) .create(); - let acc = ServiceAccountAuthenticator::builder(key.clone()) - .build() - .unwrap(); + let acc = ServiceAccountFlow::new(ServiceAccountFlowOpts { + key: key.clone(), + subject: None, + }) + .unwrap(); let tok = acc - .token(&["https://www.googleapis.com/auth/pubsub"]) - .await - .expect("token failed"); - assert!(tok.access_token.contains("ya29.c.ElouBywiys0Ly")); - assert_eq!(Some(3600), tok.expires_in); - - assert!(acc - .cache - .get(storage::ScopesAndFilter::from(&[ - "https://www.googleapis.com/auth/pubsub" - ])) - .is_some()); - // Test that token is in cache (otherwise mock will tell us) - let tok = acc - .token(&["https://www.googleapis.com/auth/pubsub"]) + .token(&client, &["https://www.googleapis.com/auth/pubsub"]) .await .expect("token failed"); assert!(tok.access_token.contains("ya29.c.ElouBywiys0Ly")); @@ -419,10 +313,14 @@ mod tests { .with_header("content-type", "text/json") .with_body(bad_json_response.to_string()) .create(); - let acc = ServiceAccountAuthenticator::builder(key.clone()) - .build() - .unwrap(); - let result = acc.token(&["https://www.googleapis.com/auth/pubsub"]).await; + let acc = ServiceAccountFlow::new(ServiceAccountFlowOpts { + key: key.clone(), + subject: None, + }) + .unwrap(); + let result = acc + .token(&client, &["https://www.googleapis.com/auth/pubsub"]) + .await; assert!(result.is_err()); _m.assert(); } @@ -436,10 +334,15 @@ mod tests { #[allow(dead_code)] async fn test_service_account_e2e() { let key = service_account_key_from_file(&TEST_PRIVATE_KEY_PATH.to_string()).unwrap(); - let acc = ServiceAccountAuthenticator::builder(key).build().unwrap(); + let acc = ServiceAccountFlow::new(ServiceAccountFlowOpts { key, subject: None }).unwrap(); + let https = HttpsConnector::new(); + let client = hyper::Client::builder() + .keep_alive(false) + .build::<_, hyper::Body>(https); println!( "{:?}", - acc.token(&["https://www.googleapis.com/auth/pubsub"]).await + acc.token(&client, &["https://www.googleapis.com/auth/pubsub"]) + .await ); } diff --git a/src/storage.rs b/src/storage.rs index 35e123a..3172c85 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -178,27 +178,24 @@ impl JSONTokens { // application will provide the same set of scopes repeatedly. If a // token exists for the exact scope list requested a lookup of the // ScopeFilter will return a list that would contain it. - if let Some(tokens) = self.token_map.get(&filter) { - for t in tokens { - if requested_scopes_are_subset_of(t.scopes.as_slice()) { - return Some(t.token.clone()); - } - } + if let Some(t) = self + .token_map + .get(&filter) + .into_iter() + .flat_map(|tokens_matching_filter| tokens_matching_filter.iter()) + .find(|js_token: &&JSONToken| requested_scopes_are_subset_of(&js_token.scopes)) + { + return Some(t.token.clone()); } // No exact match for the scopes provided. Search for any tokens that // exist for a superset of the scopes requested. - for t in self - .token_map + self.token_map .iter() - .filter(|(k, _v)| filter.is_subset_of(**k) == FilterResponse::Maybe) - .flat_map(|(_, v)| v.iter()) - { - if requested_scopes_are_subset_of(&t.scopes) { - return Some(t.token.clone()); - } - } - None + .filter(|(k, _)| filter.is_subset_of(**k) == FilterResponse::Maybe) + .flat_map(|(_, tokens_matching_filter)| tokens_matching_filter.iter()) + .find(|v: &&JSONToken| requested_scopes_are_subset_of(&v.scopes)) + .map(|t: &JSONToken| t.token.clone()) } fn set(&mut self, ScopesAndFilter { filter, scopes }: ScopesAndFilter, token: Token)