-
-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make the public API more modular #49
Changes from all commits
cf0ffba
148eb79
a0df627
d5c543a
a73537c
0e6fd5a
ee892c3
8976520
4240558
9da6f85
8076fdb
11c1d9e
f95113c
ed6203b
a220fad
a4da35a
467c076
b7c0fd5
8b8c07e
4c688ca
c29a7df
6c2c460
c0dc296
562795d
88ae957
b62d18b
3963a98
b312228
7e202ee
3c6185d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
target | ||
Cargo.lock | ||
.DS_Store | ||
|
||
# Intellij project files | ||
*.iml | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,78 @@ | ||
use std::collections::HashMap; | ||
use std::path::Path; | ||
use std::path::{Path, PathBuf}; | ||
use std::sync::RwLock; | ||
|
||
use async_trait::async_trait; | ||
use serde::{Deserialize, Serialize}; | ||
use tokio::fs; | ||
|
||
use crate::authentication_manager::ServiceAccount; | ||
use crate::error::Error; | ||
use crate::types::{HyperClient, Token}; | ||
use crate::types::{HyperClient, Signer, Token}; | ||
use crate::util::HyperExt; | ||
|
||
/// A custom service account containing credentials | ||
/// | ||
/// Once initialized, a [`CustomServiceAccount`] can be converted into an [`AuthenticationManager`] | ||
/// using the applicable `From` implementation. | ||
#[derive(Debug)] | ||
pub(crate) struct CustomServiceAccount { | ||
tokens: RwLock<HashMap<Vec<String>, Token>>, | ||
pub struct CustomServiceAccount { | ||
credentials: ApplicationCredentials, | ||
signer: Signer, | ||
tokens: RwLock<HashMap<Vec<String>, Token>>, | ||
} | ||
|
||
impl CustomServiceAccount { | ||
pub(crate) async fn from_file(path: &Path) -> Result<Self, Error> { | ||
Ok(Self { | ||
credentials: ApplicationCredentials::from_file(path).await?, | ||
tokens: RwLock::new(HashMap::new()), | ||
}) | ||
/// Check `GOOGLE_APPLICATION_CREDENTIALS` environment variable for a path to JSON credentials | ||
pub fn from_env() -> Result<Option<Self>, Error> { | ||
std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS") | ||
.map(|path| { | ||
tracing::debug!( | ||
"Reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var" | ||
); | ||
Self::from_file(&PathBuf::from(path)) | ||
}) | ||
.transpose() | ||
} | ||
|
||
/// Read service account credentials from the given JSON file | ||
pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, Error> { | ||
let file = std::fs::File::open(path.as_ref()).map_err(Error::CustomServiceAccountPath)?; | ||
match serde_json::from_reader::<_, ApplicationCredentials>(file) { | ||
Ok(credentials) => Self::new(credentials), | ||
Err(e) => Err(Error::CustomServiceAccountCredentials(e)), | ||
} | ||
} | ||
|
||
/// Read service account credentials from the given JSON string | ||
pub fn from_json(s: &str) -> Result<Self, Error> { | ||
match serde_json::from_str::<ApplicationCredentials>(s) { | ||
Ok(credentials) => Self::new(credentials), | ||
Err(e) => Err(Error::CustomServiceAccountCredentials(e)), | ||
} | ||
} | ||
|
||
pub(crate) fn from_json(s: &str) -> Result<Self, Error> { | ||
fn new(credentials: ApplicationCredentials) -> Result<Self, Error> { | ||
Ok(Self { | ||
credentials: ApplicationCredentials::from_json(s)?, | ||
signer: Signer::new(&credentials.private_key)?, | ||
credentials, | ||
tokens: RwLock::new(HashMap::new()), | ||
}) | ||
} | ||
|
||
/// The RSA PKCS1 SHA256 [`Signer`] used to sign JWT tokens | ||
pub fn signer(&self) -> &Signer { | ||
&self.signer | ||
} | ||
|
||
/// The project ID as found in the credentials | ||
pub fn project_id(&self) -> Option<&str> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems odd to me to provide two public functions with the same name on the same struct. I realise this is a slightly different signature that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
self.credentials.project_id.as_deref() | ||
} | ||
|
||
/// The private key as found in the credentials | ||
pub fn private_key_pem(&self) -> &str { | ||
&self.credentials.private_key | ||
} | ||
} | ||
|
||
#[async_trait] | ||
|
@@ -47,31 +89,31 @@ impl ServiceAccount for CustomServiceAccount { | |
self.tokens.read().unwrap().get(&key).cloned() | ||
} | ||
|
||
#[tracing::instrument] | ||
async fn refresh_token(&self, client: &HyperClient, scopes: &[&str]) -> Result<Token, Error> { | ||
use crate::jwt::Claims; | ||
use crate::jwt::JwtSigner; | ||
use crate::jwt::GRANT_TYPE; | ||
use hyper::header; | ||
use url::form_urlencoded; | ||
|
||
let signer = JwtSigner::new(&self.credentials.private_key)?; | ||
|
||
let claims = Claims::new(&self.credentials, scopes, None); | ||
let signed = signer.sign_claims(&claims).map_err(Error::TLSError)?; | ||
let jwt = Claims::new(&self.credentials, scopes, None).to_jwt(&self.signer)?; | ||
let rqbody = form_urlencoded::Serializer::new(String::new()) | ||
.extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", signed.as_str())]) | ||
.extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", jwt.as_str())]) | ||
.finish(); | ||
|
||
let request = hyper::Request::post(&self.credentials.token_uri) | ||
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") | ||
.body(hyper::Body::from(rqbody)) | ||
.unwrap(); | ||
log::debug!("requesting token from service account: {:?}", request); | ||
|
||
tracing::debug!("requesting token from service account: {:?}", request); | ||
let token = client | ||
.request(request) | ||
.await | ||
.map_err(Error::OAuthConnectionError)? | ||
.deserialize::<Token>() | ||
.await?; | ||
|
||
let key = scopes.iter().map(|x| (*x).to_string()).collect(); | ||
self.tokens.write().unwrap().insert(key, token.clone()); | ||
Ok(token) | ||
|
@@ -100,16 +142,3 @@ pub(crate) struct ApplicationCredentials { | |
/// client_x509_cert_url | ||
pub(crate) client_x509_cert_url: Option<String>, | ||
} | ||
|
||
impl ApplicationCredentials { | ||
async fn from_file<T: AsRef<Path>>(path: T) -> Result<ApplicationCredentials, Error> { | ||
let content = fs::read_to_string(path) | ||
.await | ||
.map_err(Error::ApplicationProfilePath)?; | ||
ApplicationCredentials::from_json(&content) | ||
} | ||
|
||
fn from_json(s: &str) -> Result<ApplicationCredentials, Error> { | ||
serde_json::from_str(s).map_err(Error::ApplicationProfileFormat) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why make the signer public?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So there's a use case here: ThouCheese/cloud-storage-rs#92. This is for the https://cloud.google.com/storage/docs/access-control/signed-urls feature, which uses the same encryption mechanism as far as I can tell.
More broadly, this is basically code that we already have in gcp_auth, so we might as well expose it to users as an easy API -- they might also want to produce JWTs.