From e749b02a817dae56349d327329cad8d88ae59d6c Mon Sep 17 00:00:00 2001 From: ynqa Date: Wed, 20 Mar 2019 02:21:14 +0900 Subject: [PATCH 1/4] Add auth_provider for cloud --- src/config/apis.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/config/apis.rs b/src/config/apis.rs index 775cc0286..586f58420 100644 --- a/src/config/apis.rs +++ b/src/config/apis.rs @@ -1,5 +1,6 @@ use std::fs::File; use std::path::Path; +use std::collections::HashMap; use failure::Error; use serde_yaml; @@ -87,6 +88,16 @@ pub struct AuthInfo { pub impersonate: Option, #[serde(rename = "as-groups")] pub impersonate_groups: Option>, + + #[serde(rename = "auth-provider")] + pub auth_provider: Option, +} + +/// AuthProviderConfig stores auth for specified cloud provider +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthProviderConfig { + pub name: String, + pub config: HashMap, } /// NamedContext associates name with context. From 9633021a7aece4f00e81894a4331f3621b6c2837 Mon Sep 17 00:00:00 2001 From: ynqa Date: Thu, 18 Apr 2019 20:44:16 +0900 Subject: [PATCH 2/4] Add oauth2 --- Cargo.toml | 9 ++- examples/list_pod.rs | 2 +- src/config/apis.rs | 20 ++++- src/config/kube_config.rs | 10 ++- src/config/mod.rs | 2 +- src/config/utils.rs | 9 +++ src/lib.rs | 8 ++ src/oauth2/mod.rs | 155 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 src/oauth2/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 6ad3df0cb..7950ba4e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,19 @@ categories = ["web-programming::http-client"] [dependencies] base64 = "0.9.3" +chrono = "0.4.6" dirs = "1.0.4" failure = "0.1.2" +http = "0.1.14" +lazy_static = "1.3.0" +openssl = "0.10.12" reqwest = "0.9.2" serde = "1.0.79" serde_derive = "1.0.79" +serde_json = "1.0.39" serde_yaml = "0.8.5" -openssl = "0.10.12" -http = "0.1.14" +time = "0.1.42" +url = "1.7.2" [dev-dependencies] tempfile = "3.0.4" diff --git a/examples/list_pod.rs b/examples/list_pod.rs index 01fd33f27..b466b9728 100644 --- a/examples/list_pod.rs +++ b/examples/list_pod.rs @@ -14,5 +14,5 @@ fn main() { let list_pod = kubeclient .request::(req) .expect("failed to list up pods"); - println!("{:?}", list_pod); + // println!("{:?}", list_pod); } diff --git a/src/config/apis.rs b/src/config/apis.rs index 586f58420..21d24607f 100644 --- a/src/config/apis.rs +++ b/src/config/apis.rs @@ -1,11 +1,12 @@ +use std::collections::HashMap; use std::fs::File; use std::path::Path; -use std::collections::HashMap; use failure::Error; use serde_yaml; use config::utils; +use oauth2; /// Config stores information to connect remote kubernetes cluster. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -93,7 +94,7 @@ pub struct AuthInfo { pub auth_provider: Option, } -/// AuthProviderConfig stores auth for specified cloud provider +/// AuthProviderConfig stores auth for specified cloud provider. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AuthProviderConfig { pub name: String, @@ -134,6 +135,21 @@ impl Cluster { } impl AuthInfo { + pub fn load_gcp(&mut self) -> Result { + match &self.auth_provider { + Some(provider) => { + self.token = Some(provider.config["access-token"].clone()); + if utils::is_expired(&provider.config["expiry"]) { + let client = oauth2::CredentialsClient::new()?; + let token = client.request_token(&vec!["https://www.googleapis.com/auth/cloud-platform".to_string()])?; + self.token = Some(token.access_token); + } + } + None => {} + }; + Ok(true) + } + pub fn load_client_certificate(&self) -> Result, Error> { utils::data_or_file_with_base64(&self.client_certificate_data, &self.client_certificate) } diff --git a/src/config/kube_config.rs b/src/config/kube_config.rs index 7b1b4c68b..83eee0534 100644 --- a/src/config/kube_config.rs +++ b/src/config/kube_config.rs @@ -34,8 +34,14 @@ impl KubeConfigLoader { .auth_infos .iter() .find(|named_user| named_user.name == current_context.user) - .map(|named_user| &named_user.auth_info) - .ok_or(format_err!("Unable to load user of current context"))?; + .map(|named_user| { + let mut user = named_user.auth_info.clone(); + match user.load_gcp() { + Ok(_) => Ok(user), + Err(e) => Err(e), + } + }) + .ok_or(format_err!("Unable to load user of current context"))??; Ok(KubeConfigLoader { current_context: current_context.clone(), cluster: cluster.clone(), diff --git a/src/config/mod.rs b/src/config/mod.rs index dc77941b2..1e512b53f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -50,7 +50,7 @@ pub fn load_kube_config() -> Result { let req_p12 = Identity::from_pkcs12_der(&p12.to_der()?, " ")?; client_builder = client_builder.identity(req_p12); } - Err(_e) => { + Err(_) => { // last resort only if configs ask for it, and no client certs if let Some(true) = loader.cluster.insecure_skip_tls_verify { client_builder = client_builder.danger_accept_invalid_certs(true); diff --git a/src/config/utils.rs b/src/config/utils.rs index a6e766c61..4eeb5df5b 100644 --- a/src/config/utils.rs +++ b/src/config/utils.rs @@ -4,6 +4,7 @@ use std::io::Read; use std::path::{Path, PathBuf}; use base64; +use chrono::{DateTime, Utc}; use dirs::home_dir; use failure::Error; @@ -51,6 +52,14 @@ pub fn data_or_file>( } } +pub fn is_expired(timestamp: &str) -> bool { + let ts = DateTime::parse_from_rfc3339(timestamp).unwrap(); + let now = DateTime::parse_from_rfc3339(&Utc::now().to_rfc3339()).unwrap(); + println!("{:?}", ts); + println!("{:?}", now); + ts < now +} + #[test] fn test_kubeconfig_path() { let expect_str = "/fake/.kube/config"; diff --git a/src/lib.rs b/src/lib.rs index fc52ef00b..fb4f51e03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,23 @@ #[macro_use] extern crate failure; #[macro_use] +extern crate lazy_static; +#[macro_use] extern crate serde_derive; +#[macro_use] +extern crate serde_json; extern crate base64; +extern crate chrono; extern crate dirs; extern crate http; extern crate openssl; extern crate reqwest; extern crate serde; extern crate serde_yaml; +extern crate time; +extern crate url; pub mod client; pub mod config; +mod oauth2; diff --git a/src/oauth2/mod.rs b/src/oauth2/mod.rs new file mode 100644 index 000000000..b2b9cd719 --- /dev/null +++ b/src/oauth2/mod.rs @@ -0,0 +1,155 @@ +use std::env; +use std::fs::File; +use std::path::PathBuf; + +use chrono::Utc; +use failure::Error; +use openssl::pkey::PKey; +use openssl::sign::Signer; +use openssl::rsa::Padding; +use openssl::hash::MessageDigest; +use reqwest::Client; +use reqwest::header::CONTENT_TYPE; +use time::Duration; +use url::form_urlencoded::Serializer; + +const GOOGLE_APPLICATION_CREDENTIALS: &str = "GOOGLE_APPLICATION_CREDENTIALS"; +const DEFAULT_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer"; +lazy_static! { + static ref DEFAULT_HEADER: String = json!({"alg": "RS256","typ": "JWT"}).to_string(); +} + +// https://github.com/golang/oauth2/blob/c85d3e98c914e3a33234ad863dcbff5dbc425bb8/jws/jws.go#L34-L52 +#[derive(Debug, Serialize)] +struct Claim { + iss: String, + scope: String, + aud: String, + exp: i64, + iat: i64, +} + +impl Claim { + fn new(c: &Credentials, scope: &Vec) -> Claim { + let iat = Utc::now(); + // The access token is available for 1 hour. + // https://github.com/golang/oauth2/blob/c85d3e98c914e3a33234ad863dcbff5dbc425bb8/jws/jws.go#L63 + let exp = iat + Duration::hours(1); + Claim { + iss: c.client_email.clone(), + scope: scope.join(" "), + aud: c.token_uri.clone(), + exp: exp.timestamp(), + iat: iat.timestamp(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Credentials { + #[serde(rename = "type")] + typ: String, + project_id: String, + private_key_id: String, + private_key: String, + client_email: String, + client_id: String, + auth_uri: String, + token_uri: String, + auth_provider_x509_cert_url: String, + client_x509_cert_url: String, +} + +impl Credentials { + pub fn load() -> Result { + let path = env::var_os(GOOGLE_APPLICATION_CREDENTIALS) + .map(PathBuf::from) + .ok_or(format_err!( + "Missing {} env", + GOOGLE_APPLICATION_CREDENTIALS + ))?; + let f = File::open(path)?; + let config = serde_json::from_reader(f)?; + Ok(config) + } +} + +pub struct CredentialsClient { + pub credentials: Credentials, + pub client: Client, +} + +// https://github.com/golang/oauth2/blob/c85d3e98c914e3a33234ad863dcbff5dbc425bb8/internal/token.go#L61-L66 +#[derive(Debug, Serialize, Deserialize)] +struct TokenResponse { + access_token: Option, + token_type: Option, + expires_in: Option, +} + +impl TokenResponse { + pub fn to_token(self) -> Token { + Token { + access_token: self.access_token.unwrap(), + token_type: self.token_type.unwrap(), + refresh_token: String::new(), + expiry: self.expires_in, + } + } +} + +// https://github.com/golang/oauth2/blob/c85d3e98c914e3a33234ad863dcbff5dbc425bb8/token.go#L31-L55 +#[derive(Debug)] +pub struct Token { + pub access_token: String, + pub token_type: String, + pub refresh_token: String, + pub expiry: Option, +} + +impl CredentialsClient { + pub fn new() -> Result { + Ok(CredentialsClient { + credentials: Credentials::load()?, + client: Client::new(), + }) + } + pub fn request_token(&self, scopes: &Vec) -> Result { + let header = &self.jwt_header(scopes)?; + let body = Serializer::new(String::new()) + .extend_pairs(vec![ + ("grant_type".to_string(), DEFAULT_GRANT_TYPE.to_string()), + ("assertion".to_string(), header.to_string()), + ]).finish(); + println!("{:?}", body); + let token_response: TokenResponse = self.client + .post(&self.credentials.token_uri) + .body(body) + .header(CONTENT_TYPE, "application/x-www-form-urlencoded") + .send()? + .json()?; + println!("{:?}", token_response); + Ok(token_response.to_token()) + } + + fn jwt_header(&self, scopes: &Vec) -> Result { + let claim = Claim::new(&self.credentials, scopes); + let header = &self.jwt_encode(&claim)?; + let private = &self.credentials.private_key.to_string().replace("\\n", "\n").into_bytes(); + let decoded = PKey::private_key_from_pem(private)?; + let mut signer = Signer::new(MessageDigest::sha256(), &decoded)?; + signer.set_rsa_padding(Padding::PKCS1)?; + signer.update(header.as_bytes())?; + let signature = signer.sign_to_vec()?; + let encoded = base64::encode_config(&signature, base64::URL_SAFE); + Ok([header.to_string(), ".".to_string(), encoded].join("")) + } + + fn jwt_encode(&self, claim: &Claim) -> Result { + let header = [ + base64::encode_config(GOOGLE_APPLICATION_CREDENTIALS, base64::URL_SAFE), + ".".to_string(), + base64::encode_config(&serde_json::to_string(claim)?, base64::URL_SAFE)].join(""); + Ok(header) + } +} From b237b75b061d7e7c4f42b81ca6d6f6d261c5e9de Mon Sep 17 00:00:00 2001 From: ynqa Date: Thu, 18 Apr 2019 22:55:18 +0900 Subject: [PATCH 3/4] Fix signature --- examples/list_pod.rs | 2 +- src/config/utils.rs | 2 -- src/oauth2/mod.rs | 48 ++++++++++++++++++++++++-------------------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/examples/list_pod.rs b/examples/list_pod.rs index b466b9728..01fd33f27 100644 --- a/examples/list_pod.rs +++ b/examples/list_pod.rs @@ -14,5 +14,5 @@ fn main() { let list_pod = kubeclient .request::(req) .expect("failed to list up pods"); - // println!("{:?}", list_pod); + println!("{:?}", list_pod); } diff --git a/src/config/utils.rs b/src/config/utils.rs index 4eeb5df5b..1923c5c23 100644 --- a/src/config/utils.rs +++ b/src/config/utils.rs @@ -55,8 +55,6 @@ pub fn data_or_file>( pub fn is_expired(timestamp: &str) -> bool { let ts = DateTime::parse_from_rfc3339(timestamp).unwrap(); let now = DateTime::parse_from_rfc3339(&Utc::now().to_rfc3339()).unwrap(); - println!("{:?}", ts); - println!("{:?}", now); ts < now } diff --git a/src/oauth2/mod.rs b/src/oauth2/mod.rs index b2b9cd719..570eb314d 100644 --- a/src/oauth2/mod.rs +++ b/src/oauth2/mod.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use chrono::Utc; use failure::Error; -use openssl::pkey::PKey; +use openssl::pkey::{PKey, Private}; use openssl::sign::Signer; use openssl::rsa::Padding; use openssl::hash::MessageDigest; @@ -15,8 +15,12 @@ use url::form_urlencoded::Serializer; const GOOGLE_APPLICATION_CREDENTIALS: &str = "GOOGLE_APPLICATION_CREDENTIALS"; const DEFAULT_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer"; -lazy_static! { - static ref DEFAULT_HEADER: String = json!({"alg": "RS256","typ": "JWT"}).to_string(); + + +#[derive(Debug, Serialize)] +struct Header { + alg: String, + typ: String, } // https://github.com/golang/oauth2/blob/c85d3e98c914e3a33234ad863dcbff5dbc425bb8/jws/jws.go#L34-L52 @@ -115,41 +119,41 @@ impl CredentialsClient { }) } pub fn request_token(&self, scopes: &Vec) -> Result { - let header = &self.jwt_header(scopes)?; + let private_key = PKey::private_key_from_pem(&self.credentials.private_key.as_bytes())?; + let encoded = &self.jws_encode( + &Claim::new(&self.credentials, scopes), + &Header{ + alg: "RS256".to_string(), + typ: "JWT".to_string(), + }, + private_key)?; + let body = Serializer::new(String::new()) .extend_pairs(vec![ ("grant_type".to_string(), DEFAULT_GRANT_TYPE.to_string()), - ("assertion".to_string(), header.to_string()), + ("assertion".to_string(), encoded.to_string()), ]).finish(); - println!("{:?}", body); let token_response: TokenResponse = self.client .post(&self.credentials.token_uri) .body(body) .header(CONTENT_TYPE, "application/x-www-form-urlencoded") .send()? .json()?; - println!("{:?}", token_response); Ok(token_response.to_token()) } - fn jwt_header(&self, scopes: &Vec) -> Result { - let claim = Claim::new(&self.credentials, scopes); - let header = &self.jwt_encode(&claim)?; - let private = &self.credentials.private_key.to_string().replace("\\n", "\n").into_bytes(); - let decoded = PKey::private_key_from_pem(private)?; - let mut signer = Signer::new(MessageDigest::sha256(), &decoded)?; + fn jws_encode(&self, claim: &Claim, header: &Header, key: PKey) -> Result { + let encoded_header = self.base64_encode(serde_json::to_string(&header).unwrap().as_bytes()); + let encoded_claims = self.base64_encode(serde_json::to_string(&claim).unwrap().as_bytes()); + let signature_base = format!("{}.{}", encoded_header, encoded_claims); + let mut signer = Signer::new(MessageDigest::sha256(), &key)?; signer.set_rsa_padding(Padding::PKCS1)?; - signer.update(header.as_bytes())?; + signer.update(signature_base.as_bytes())?; let signature = signer.sign_to_vec()?; - let encoded = base64::encode_config(&signature, base64::URL_SAFE); - Ok([header.to_string(), ".".to_string(), encoded].join("")) + Ok(format!("{}.{}", signature_base, self.base64_encode(&signature))) } - fn jwt_encode(&self, claim: &Claim) -> Result { - let header = [ - base64::encode_config(GOOGLE_APPLICATION_CREDENTIALS, base64::URL_SAFE), - ".".to_string(), - base64::encode_config(&serde_json::to_string(claim)?, base64::URL_SAFE)].join(""); - Ok(header) + fn base64_encode(&self, bytes: &[u8]) -> String { + base64::encode_config(bytes, base64::URL_SAFE) } } From 4e448a5b685d1837a808cc4b6d5e30dc67cb560a Mon Sep 17 00:00:00 2001 From: ynqa Date: Fri, 19 Apr 2019 15:33:56 +0900 Subject: [PATCH 4/4] Add unused macro --- src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fb4f51e03..d877fa5c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,7 @@ #[macro_use] extern crate failure; #[macro_use] -extern crate lazy_static; -#[macro_use] extern crate serde_derive; -#[macro_use] -extern crate serde_json; extern crate base64; extern crate chrono;