mirror of
https://github.com/OMGeeky/yup-oauth2.git
synced 2026-01-05 10:50:36 +01:00
Merge pull request #185 from jneem/master
Implement service account impersonation.
This commit is contained in:
@@ -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"
|
||||
|
||||
23
examples/service_account_impersonation.rs
Normal file
23
examples/service_account_impersonation.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
128
src/service_account_impersonator.rs
Normal file
128
src/service_account_impersonator.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user