Skip to content

Commit d3e0f4e

Browse files
authored
Merge pull request #16 from ynqa/gcp
Add auth_provider for cloud
2 parents 10beef1 + 4e448a5 commit d3e0f4e

File tree

7 files changed

+213
-5
lines changed

7 files changed

+213
-5
lines changed

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@ categories = ["web-programming::http-client"]
1111

1212
[dependencies]
1313
base64 = "0.9.3"
14+
chrono = "0.4.6"
1415
dirs = "1.0.4"
1516
failure = "0.1.2"
17+
http = "0.1.14"
18+
lazy_static = "1.3.0"
19+
openssl = "0.10.12"
1620
reqwest = "0.9.2"
1721
serde = "1.0.79"
1822
serde_derive = "1.0.79"
23+
serde_json = "1.0.39"
1924
serde_yaml = "0.8.5"
20-
openssl = "0.10.12"
21-
http = "0.1.14"
25+
time = "0.1.42"
26+
url = "1.7.2"
2227

2328
[dev-dependencies]
2429
tempfile = "3.0.4"

src/config/apis.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use std::collections::HashMap;
12
use std::fs::File;
23
use std::path::Path;
34

45
use failure::Error;
56
use serde_yaml;
67

78
use config::utils;
9+
use oauth2;
810

911
/// Config stores information to connect remote kubernetes cluster.
1012
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -87,6 +89,16 @@ pub struct AuthInfo {
8789
pub impersonate: Option<String>,
8890
#[serde(rename = "as-groups")]
8991
pub impersonate_groups: Option<Vec<String>>,
92+
93+
#[serde(rename = "auth-provider")]
94+
pub auth_provider: Option<AuthProviderConfig>,
95+
}
96+
97+
/// AuthProviderConfig stores auth for specified cloud provider.
98+
#[derive(Clone, Debug, Serialize, Deserialize)]
99+
pub struct AuthProviderConfig {
100+
pub name: String,
101+
pub config: HashMap<String, String>,
90102
}
91103

92104
/// NamedContext associates name with context.
@@ -127,6 +139,21 @@ impl Cluster {
127139
}
128140

129141
impl AuthInfo {
142+
pub fn load_gcp(&mut self) -> Result<bool, Error> {
143+
match &self.auth_provider {
144+
Some(provider) => {
145+
self.token = Some(provider.config["access-token"].clone());
146+
if utils::is_expired(&provider.config["expiry"]) {
147+
let client = oauth2::CredentialsClient::new()?;
148+
let token = client.request_token(&vec!["https://www.googleapis.com/auth/cloud-platform".to_string()])?;
149+
self.token = Some(token.access_token);
150+
}
151+
}
152+
None => {}
153+
};
154+
Ok(true)
155+
}
156+
130157
pub fn load_client_certificate(&self) -> Result<Vec<u8>, Error> {
131158
utils::data_or_file_with_base64(&self.client_certificate_data, &self.client_certificate)
132159
}

src/config/kube_config.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@ impl KubeConfigLoader {
3434
.auth_infos
3535
.iter()
3636
.find(|named_user| named_user.name == current_context.user)
37-
.map(|named_user| &named_user.auth_info)
38-
.ok_or(format_err!("Unable to load user of current context"))?;
37+
.map(|named_user| {
38+
let mut user = named_user.auth_info.clone();
39+
match user.load_gcp() {
40+
Ok(_) => Ok(user),
41+
Err(e) => Err(e),
42+
}
43+
})
44+
.ok_or(format_err!("Unable to load user of current context"))??;
3945
Ok(KubeConfigLoader {
4046
current_context: current_context.clone(),
4147
cluster: cluster.clone(),

src/config/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub fn load_kube_config() -> Result<Configuration, Error> {
5050
let req_p12 = Identity::from_pkcs12_der(&p12.to_der()?, " ")?;
5151
client_builder = client_builder.identity(req_p12);
5252
}
53-
Err(_e) => {
53+
Err(_) => {
5454
// last resort only if configs ask for it, and no client certs
5555
if let Some(true) = loader.cluster.insecure_skip_tls_verify {
5656
client_builder = client_builder.danger_accept_invalid_certs(true);

src/config/utils.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::io::Read;
44
use std::path::{Path, PathBuf};
55

66
use base64;
7+
use chrono::{DateTime, Utc};
78
use dirs::home_dir;
89
use failure::Error;
910

@@ -51,6 +52,12 @@ pub fn data_or_file<P: AsRef<Path>>(
5152
}
5253
}
5354

55+
pub fn is_expired(timestamp: &str) -> bool {
56+
let ts = DateTime::parse_from_rfc3339(timestamp).unwrap();
57+
let now = DateTime::parse_from_rfc3339(&Utc::now().to_rfc3339()).unwrap();
58+
ts < now
59+
}
60+
5461
#[test]
5562
fn test_kubeconfig_path() {
5663
let expect_str = "/fake/.kube/config";

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ extern crate failure;
44
extern crate serde_derive;
55

66
extern crate base64;
7+
extern crate chrono;
78
extern crate dirs;
89
extern crate http;
910
extern crate openssl;
1011
extern crate reqwest;
1112
extern crate serde;
1213
extern crate serde_yaml;
14+
extern crate time;
15+
extern crate url;
1316

1417
pub mod client;
1518
pub mod config;
19+
mod oauth2;

src/oauth2/mod.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use std::env;
2+
use std::fs::File;
3+
use std::path::PathBuf;
4+
5+
use chrono::Utc;
6+
use failure::Error;
7+
use openssl::pkey::{PKey, Private};
8+
use openssl::sign::Signer;
9+
use openssl::rsa::Padding;
10+
use openssl::hash::MessageDigest;
11+
use reqwest::Client;
12+
use reqwest::header::CONTENT_TYPE;
13+
use time::Duration;
14+
use url::form_urlencoded::Serializer;
15+
16+
const GOOGLE_APPLICATION_CREDENTIALS: &str = "GOOGLE_APPLICATION_CREDENTIALS";
17+
const DEFAULT_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
18+
19+
20+
#[derive(Debug, Serialize)]
21+
struct Header {
22+
alg: String,
23+
typ: String,
24+
}
25+
26+
// https://github.com/golang/oauth2/blob/c85d3e98c914e3a33234ad863dcbff5dbc425bb8/jws/jws.go#L34-L52
27+
#[derive(Debug, Serialize)]
28+
struct Claim {
29+
iss: String,
30+
scope: String,
31+
aud: String,
32+
exp: i64,
33+
iat: i64,
34+
}
35+
36+
impl Claim {
37+
fn new(c: &Credentials, scope: &Vec<String>) -> Claim {
38+
let iat = Utc::now();
39+
// The access token is available for 1 hour.
40+
// https://github.com/golang/oauth2/blob/c85d3e98c914e3a33234ad863dcbff5dbc425bb8/jws/jws.go#L63
41+
let exp = iat + Duration::hours(1);
42+
Claim {
43+
iss: c.client_email.clone(),
44+
scope: scope.join(" "),
45+
aud: c.token_uri.clone(),
46+
exp: exp.timestamp(),
47+
iat: iat.timestamp(),
48+
}
49+
}
50+
}
51+
52+
#[derive(Clone, Debug, Serialize, Deserialize)]
53+
pub struct Credentials {
54+
#[serde(rename = "type")]
55+
typ: String,
56+
project_id: String,
57+
private_key_id: String,
58+
private_key: String,
59+
client_email: String,
60+
client_id: String,
61+
auth_uri: String,
62+
token_uri: String,
63+
auth_provider_x509_cert_url: String,
64+
client_x509_cert_url: String,
65+
}
66+
67+
impl Credentials {
68+
pub fn load() -> Result<Credentials, Error> {
69+
let path = env::var_os(GOOGLE_APPLICATION_CREDENTIALS)
70+
.map(PathBuf::from)
71+
.ok_or(format_err!(
72+
"Missing {} env",
73+
GOOGLE_APPLICATION_CREDENTIALS
74+
))?;
75+
let f = File::open(path)?;
76+
let config = serde_json::from_reader(f)?;
77+
Ok(config)
78+
}
79+
}
80+
81+
pub struct CredentialsClient {
82+
pub credentials: Credentials,
83+
pub client: Client,
84+
}
85+
86+
// https://github.com/golang/oauth2/blob/c85d3e98c914e3a33234ad863dcbff5dbc425bb8/internal/token.go#L61-L66
87+
#[derive(Debug, Serialize, Deserialize)]
88+
struct TokenResponse {
89+
access_token: Option<String>,
90+
token_type: Option<String>,
91+
expires_in: Option<i64>,
92+
}
93+
94+
impl TokenResponse {
95+
pub fn to_token(self) -> Token {
96+
Token {
97+
access_token: self.access_token.unwrap(),
98+
token_type: self.token_type.unwrap(),
99+
refresh_token: String::new(),
100+
expiry: self.expires_in,
101+
}
102+
}
103+
}
104+
105+
// https://github.com/golang/oauth2/blob/c85d3e98c914e3a33234ad863dcbff5dbc425bb8/token.go#L31-L55
106+
#[derive(Debug)]
107+
pub struct Token {
108+
pub access_token: String,
109+
pub token_type: String,
110+
pub refresh_token: String,
111+
pub expiry: Option<i64>,
112+
}
113+
114+
impl CredentialsClient {
115+
pub fn new() -> Result<CredentialsClient, Error> {
116+
Ok(CredentialsClient {
117+
credentials: Credentials::load()?,
118+
client: Client::new(),
119+
})
120+
}
121+
pub fn request_token(&self, scopes: &Vec<String>) -> Result<Token, Error> {
122+
let private_key = PKey::private_key_from_pem(&self.credentials.private_key.as_bytes())?;
123+
let encoded = &self.jws_encode(
124+
&Claim::new(&self.credentials, scopes),
125+
&Header{
126+
alg: "RS256".to_string(),
127+
typ: "JWT".to_string(),
128+
},
129+
private_key)?;
130+
131+
let body = Serializer::new(String::new())
132+
.extend_pairs(vec![
133+
("grant_type".to_string(), DEFAULT_GRANT_TYPE.to_string()),
134+
("assertion".to_string(), encoded.to_string()),
135+
]).finish();
136+
let token_response: TokenResponse = self.client
137+
.post(&self.credentials.token_uri)
138+
.body(body)
139+
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
140+
.send()?
141+
.json()?;
142+
Ok(token_response.to_token())
143+
}
144+
145+
fn jws_encode(&self, claim: &Claim, header: &Header, key: PKey<Private>) -> Result<String, Error> {
146+
let encoded_header = self.base64_encode(serde_json::to_string(&header).unwrap().as_bytes());
147+
let encoded_claims = self.base64_encode(serde_json::to_string(&claim).unwrap().as_bytes());
148+
let signature_base = format!("{}.{}", encoded_header, encoded_claims);
149+
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
150+
signer.set_rsa_padding(Padding::PKCS1)?;
151+
signer.update(signature_base.as_bytes())?;
152+
let signature = signer.sign_to_vec()?;
153+
Ok(format!("{}.{}", signature_base, self.base64_encode(&signature)))
154+
}
155+
156+
fn base64_encode(&self, bytes: &[u8]) -> String {
157+
base64::encode_config(bytes, base64::URL_SAFE)
158+
}
159+
}

0 commit comments

Comments
 (0)