Skip to content

Commit

Permalink
Add oauth2
Browse files Browse the repository at this point in the history
  • Loading branch information
ynqa committed Apr 18, 2019
1 parent e749b02 commit 9633021
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 8 deletions.
9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion examples/list_pod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ fn main() {
let list_pod = kubeclient
.request::<api::PodList>(req)
.expect("failed to list up pods");
println!("{:?}", list_pod);
// println!("{:?}", list_pod);
}
20 changes: 18 additions & 2 deletions src/config/apis.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -93,7 +94,7 @@ pub struct AuthInfo {
pub auth_provider: Option<AuthProviderConfig>,
}

/// 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,
Expand Down Expand Up @@ -134,6 +135,21 @@ impl Cluster {
}

impl AuthInfo {
pub fn load_gcp(&mut self) -> Result<bool, Error> {
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<Vec<u8>, Error> {
utils::data_or_file_with_base64(&self.client_certificate_data, &self.client_certificate)
}
Expand Down
10 changes: 8 additions & 2 deletions src/config/kube_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub fn load_kube_config() -> Result<Configuration, Error> {
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);
Expand Down
9 changes: 9 additions & 0 deletions src/config/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -51,6 +52,14 @@ pub fn data_or_file<P: AsRef<Path>>(
}
}

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";
Expand Down
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
155 changes: 155 additions & 0 deletions src/oauth2/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String>) -> 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<Credentials, Error> {
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<String>,
token_type: Option<String>,
expires_in: Option<i64>,
}

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<i64>,
}

impl CredentialsClient {
pub fn new() -> Result<CredentialsClient, Error> {
Ok(CredentialsClient {
credentials: Credentials::load()?,
client: Client::new(),
})
}
pub fn request_token(&self, scopes: &Vec<String>) -> Result<Token, Error> {
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<String>) -> Result<String, Error> {
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<String, Error> {
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)
}
}

0 comments on commit 9633021

Please sign in to comment.