Merge pull request #185 from jneem/master

Implement service account impersonation.
This commit is contained in:
Lewin Bormann
2022-10-21 16:37:45 +00:00
committed by GitHub
6 changed files with 257 additions and 24 deletions

View File

@@ -47,7 +47,7 @@ rustls-pemfile = { version = "0.3", optional = true }
seahash = "4"
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
time = { version = "0.3.7", features = ["local-offset", "serde"] }
time = { version = "0.3.7", features = ["local-offset", "parsing", "serde"] }
tokio = { version = "1.0", features = ["fs", "macros", "io-std", "io-util", "time", "sync", "rt"] }
tower-service = "^0.3.1"
url = "2"

View File

@@ -0,0 +1,23 @@
use yup_oauth2::{read_authorized_user_secret, ServiceAccountImpersonationAuthenticator};
#[tokio::main]
async fn main() {
let svc_email = std::env::args().skip(1).next().unwrap();
let home = std::env::var("HOME").unwrap();
let user_secret =
read_authorized_user_secret(format!("{}/.config/gcloud/application_default_credentials.json", home))
.await
.expect("user secret");
let auth = ServiceAccountImpersonationAuthenticator::builder(user_secret, &svc_email)
.build()
.await
.expect("authenticator");
let scopes = &["https://www.googleapis.com/auth/youtube.readonly"];
match auth.token(scopes).await {
Err(e) => println!("error: {:?}", e),
Ok(t) => println!("token: {:?}", t),
}
}

View File

@@ -8,6 +8,7 @@ use crate::device::DeviceFlow;
use crate::error::Error;
use crate::installed::{InstalledFlow, InstalledFlowReturnMethod};
use crate::refresh::RefreshFlow;
use crate::service_account_impersonator::ServiceAccountImpersonationFlow;
#[cfg(feature = "service_account")]
use crate::service_account::{self, ServiceAccountFlow, ServiceAccountFlowOpts, ServiceAccountKey};
@@ -15,10 +16,10 @@ use crate::storage::{self, Storage, TokenStorage};
use crate::types::{AccessToken, ApplicationSecret, TokenInfo};
use private::AuthFlow;
use crate::access_token::{AccessTokenFlow};
use crate::access_token::AccessTokenFlow;
use futures::lock::Mutex;
use http::{Uri};
use http::Uri;
use hyper::client::connect::Connection;
use std::borrow::Cow;
use std::error::Error as StdError;
@@ -428,22 +429,69 @@ pub struct AccessTokenAuthenticator;
impl AccessTokenAuthenticator {
/// the builder pattern for the authenticator
pub fn builder(
access_token: String,
access_token: String,
) -> AuthenticatorBuilder<DefaultHyperClient, AccessTokenFlow> {
Self::with_client(access_token, DefaultHyperClient)
Self::with_client(access_token, DefaultHyperClient)
}
/// Construct a new Authenticator that uses the installed flow and the provided http client.
/// the client itself is not used
pub fn with_client<C>(
access_token: String,
client: C,
access_token: String,
client: C,
) -> AuthenticatorBuilder<C, AccessTokenFlow> {
AuthenticatorBuilder::new(
AccessTokenFlow {
access_token: access_token,
},
client,
)
AuthenticatorBuilder::new(
AccessTokenFlow {
access_token: access_token,
},
client,
)
}
}
/// Create a access token authenticator that uses user secrets to impersonate
/// a service account.
///
/// ```
/// # #[cfg(any(feature = "hyper-rustls", feature = "hyper-tls"))]
/// # async fn foo() {
/// # use yup_oauth2::authenticator::AuthorizedUserAuthenticator;
/// # let secret = yup_oauth2::read_authorized_user_secret("/tmp/foo").await.unwrap();
/// # let email = "my-test-account@my-test-project.iam.gserviceaccount.com";
/// let authenticator = yup_oauth2::ServiceAccountImpersonationAuthenticator::builder(secret, email)
/// .build()
/// .await
/// .expect("failed to create authenticator");
/// # }
/// ```
pub struct ServiceAccountImpersonationAuthenticator;
impl ServiceAccountImpersonationAuthenticator {
/// 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(
authorized_user_secret: AuthorizedUserSecret,
service_account_email: &str,
) -> AuthenticatorBuilder<DefaultHyperClient, ServiceAccountImpersonationFlow> {
Self::with_client(
authorized_user_secret,
service_account_email,
DefaultHyperClient,
)
}
/// Construct a new Authenticator that uses the installed flow and the provided http client.
pub fn with_client<C>(
authorized_user_secret: AuthorizedUserSecret,
service_account_email: &str,
client: C,
) -> AuthenticatorBuilder<C, ServiceAccountImpersonationFlow> {
AuthenticatorBuilder::new(
ServiceAccountImpersonationFlow::new(authorized_user_secret, service_account_email),
client,
)
}
}
@@ -706,6 +754,22 @@ impl<C> AuthenticatorBuilder<C, AuthorizedUserFlow> {
}
}
/// ## Methods available when building a service account impersonation Authenticator.
impl<C> AuthenticatorBuilder<C, ServiceAccountImpersonationFlow> {
/// Create the authenticator.
pub async fn build(self) -> io::Result<Authenticator<C::Connector>>
where
C: HyperClientBuilder,
{
Self::common_build(
self.hyper_client_builder,
self.storage_type,
AuthFlow::ServiceAccountImpersonationFlow(self.auth_flow),
)
.await
}
}
/// ## Methods available when building an access token flow Authenticator.
impl<C> AuthenticatorBuilder<C, AccessTokenFlow> {
/// Create the authenticator.
@@ -722,25 +786,27 @@ impl<C> AuthenticatorBuilder<C, AccessTokenFlow> {
}
}
mod private {
use crate::access_token::AccessTokenFlow;
use crate::application_default_credentials::ApplicationDefaultCredentialsFlow;
use crate::authorized_user::AuthorizedUserFlow;
use crate::authenticator::{AsyncRead, AsyncWrite, Connection, Service, StdError, Uri};
use crate::authorized_user::AuthorizedUserFlow;
use crate::device::DeviceFlow;
use crate::error::Error;
use crate::installed::InstalledFlow;
#[cfg(feature = "service_account")]
use crate::service_account::ServiceAccountFlow;
use crate::service_account_impersonator::ServiceAccountImpersonationFlow;
use crate::types::{ApplicationSecret, TokenInfo};
use crate::access_token::AccessTokenFlow;
pub enum AuthFlow {
DeviceFlow(DeviceFlow),
InstalledFlow(InstalledFlow),
#[cfg(feature = "service_account")]
ServiceAccountFlow(ServiceAccountFlow),
ServiceAccountImpersonationFlow(ServiceAccountImpersonationFlow),
ApplicationDefaultCredentialsFlow(ApplicationDefaultCredentialsFlow),
AuthorizedUserFlow(AuthorizedUserFlow),
AccessTokenFlow(AccessTokenFlow),
AccessTokenFlow(AccessTokenFlow),
}
impl AuthFlow {
@@ -750,9 +816,10 @@ mod private {
AuthFlow::InstalledFlow(installed_flow) => Some(&installed_flow.app_secret),
#[cfg(feature = "service_account")]
AuthFlow::ServiceAccountFlow(_) => None,
AuthFlow::ServiceAccountImpersonationFlow(_) => None,
AuthFlow::ApplicationDefaultCredentialsFlow(_) => None,
AuthFlow::AuthorizedUserFlow(_) => None,
AuthFlow::AccessTokenFlow(_) => None,
AuthFlow::AccessTokenFlow(_) => None,
}
}
@@ -777,15 +844,20 @@ mod private {
AuthFlow::ServiceAccountFlow(service_account_flow) => {
service_account_flow.token(hyper_client, scopes).await
}
AuthFlow::ServiceAccountImpersonationFlow(service_account_impersonation_flow) => {
service_account_impersonation_flow
.token(hyper_client, scopes)
.await
}
AuthFlow::ApplicationDefaultCredentialsFlow(adc_flow) => {
adc_flow.token(hyper_client, scopes).await
}
AuthFlow::AuthorizedUserFlow(authorized_user_flow) => {
authorized_user_flow.token(hyper_client, scopes).await
}
AuthFlow::AccessTokenFlow(access_token_flow) => {
access_token_flow.token(hyper_client, scopes).await
}
AuthFlow::AccessTokenFlow(access_token_flow) => {
access_token_flow.token(hyper_client, scopes).await
}
}
}
}

View File

@@ -154,6 +154,8 @@ pub enum Error {
UserError(String),
/// A lower level IO error.
LowLevelError(io::Error),
/// We required an access token, but received a response that didn't contain one.
MissingAccessToken,
/// Other errors produced by a storage provider
OtherError(anyhow::Error),
}
@@ -206,6 +208,13 @@ impl fmt::Display for Error {
}
Error::UserError(ref s) => s.fmt(f),
Error::LowLevelError(ref e) => e.fmt(f),
Error::MissingAccessToken => {
write!(
f,
"Expected an access token, but received a response without one"
)?;
Ok(())
}
Error::OtherError(ref e) => e.fmt(f),
}
}

View File

@@ -72,16 +72,17 @@
#![deny(missing_docs)]
#![cfg_attr(yup_oauth2_docsrs, feature(doc_cfg))]
pub mod access_token;
mod application_default_credentials;
pub mod authenticator;
pub mod authenticator_delegate;
pub mod authorized_user;
pub mod access_token;
mod device;
pub mod error;
mod helper;
mod installed;
mod refresh;
pub mod service_account_impersonator;
#[cfg(feature = "service_account")]
mod service_account;
@@ -97,9 +98,9 @@ mod types;
pub use crate::authenticator::ServiceAccountAuthenticator;
#[doc(inline)]
pub use crate::authenticator::{
ApplicationDefaultCredentialsAuthenticator, AuthorizedUserAuthenticator,
DeviceFlowAuthenticator, InstalledFlowAuthenticator,
AccessTokenAuthenticator,
AccessTokenAuthenticator, ApplicationDefaultCredentialsAuthenticator,
AuthorizedUserAuthenticator, DeviceFlowAuthenticator, InstalledFlowAuthenticator,
ServiceAccountImpersonationAuthenticator,
};
pub use crate::helper::*;

View File

@@ -0,0 +1,128 @@
//! This module provides an authenticator that uses authorized user secrets
//! to generate impersonated service account tokens.
//!
//! Resources:
//! - [service account impersonation](https://cloud.google.com/iam/docs/create-short-lived-credentials-direct#sa-credentials-oauth)
use http::{header, Uri};
use hyper::client::connect::Connection;
use serde::Serialize;
use std::error::Error as StdError;
use tokio::io::{AsyncRead, AsyncWrite};
use tower_service::Service;
use crate::{
authorized_user::{AuthorizedUserFlow, AuthorizedUserSecret},
storage::TokenInfo,
Error,
};
const IAM_CREDENTIALS_ENDPOINT: &'static str = "https://iamcredentials.googleapis.com";
fn uri(email: &str) -> String {
format!(
"{}/v1/projects/-/serviceAccounts/{}:generateAccessToken",
IAM_CREDENTIALS_ENDPOINT, email
)
}
#[derive(Serialize)]
struct Request<'a> {
scope: &'a [&'a str],
lifetime: &'a str,
}
// The impersonation response is in a different format from the other GCP
// responses. Why, Google, why? The response to our impersonation request.
// (Note that the naming is different from `types::AccessToken` even though
// the data is equivalent.)
#[derive(serde::Deserialize, Debug)]
struct TokenResponse {
/// The actual token
#[serde(rename = "accessToken")]
access_token: String,
/// The time until the token expires and a new one needs to be requested.
/// In RFC3339 format.
#[serde(rename = "expireTime")]
expires_time: String,
}
impl From<TokenResponse> for TokenInfo {
fn from(resp: TokenResponse) -> TokenInfo {
let expires_at = time::OffsetDateTime::parse(
&resp.expires_time,
&time::format_description::well_known::Rfc3339,
)
.ok();
TokenInfo {
access_token: Some(resp.access_token),
refresh_token: None,
expires_at,
id_token: None,
}
}
}
/// ServiceAccountImpersonationFlow uses user credentials to impersonate a service
/// account.
pub struct ServiceAccountImpersonationFlow {
pub(crate) inner_flow: AuthorizedUserFlow,
pub(crate) service_account_email: String,
}
impl ServiceAccountImpersonationFlow {
pub(crate) fn new(
user_secret: AuthorizedUserSecret,
service_account_email: &str,
) -> ServiceAccountImpersonationFlow {
ServiceAccountImpersonationFlow {
inner_flow: AuthorizedUserFlow {
secret: user_secret,
},
service_account_email: service_account_email.to_string(),
}
}
pub(crate) async fn token<S, T>(
&self,
hyper_client: &hyper::Client<S>,
scopes: &[T],
) -> Result<TokenInfo, Error>
where
T: AsRef<str>,
S: Service<Uri> + Clone + Send + Sync + 'static,
S::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
S::Future: Send + Unpin + 'static,
S::Error: Into<Box<dyn StdError + Send + Sync>>,
{
let inner_token = self
.inner_flow
.token(hyper_client, scopes)
.await?
.access_token
.ok_or(Error::MissingAccessToken)?;
let scopes: Vec<_> = scopes.iter().map(|s| s.as_ref()).collect();
let req_body = Request {
scope: &scopes,
// Max validity is 1h.
lifetime: "3600s",
};
let req_body = serde_json::to_vec(&req_body)?;
let request = hyper::Request::post(uri(&self.service_account_email))
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.header(header::CONTENT_LENGTH, req_body.len())
.header(header::AUTHORIZATION, format!("Bearer {}", inner_token))
.body(req_body.into())
.unwrap();
log::debug!("requesting impersonated token {:?}", request);
let (head, body) = hyper_client.request(request).await?.into_parts();
let body = hyper::body::to_bytes(body).await?;
log::debug!("received response; head: {:?}, body: {:?}", head, body);
let response: TokenResponse = serde_json::from_slice(&body)?;
Ok(response.into())
}
}