kube_client/client/auth/
oauth.rs

1use http_body_util::BodyExt;
2use hyper_util::rt::TokioExecutor;
3use tame_oauth::{
4    gcp::{TokenOrRequest, TokenProvider, TokenProviderWrapper},
5    Token,
6};
7use thiserror::Error;
8
9use crate::client::Body;
10
11#[derive(Error, Debug)]
12/// Possible errors when requesting token with OAuth
13pub enum Error {
14    /// Default provider appears to be configured, but was invalid
15    #[error("default provider is configured but invalid: {0}")]
16    InvalidDefaultProviderConfig(#[source] tame_oauth::Error),
17
18    /// No provider was found
19    #[error("no provider was found")]
20    NoDefaultProvider,
21
22    /// Failed to load OAuth credentials file
23    #[error("failed to load OAuth credentials file: {0}")]
24    LoadCredentials(#[source] std::io::Error),
25
26    /// Failed to parse OAuth credentials file
27    #[error("failed to parse OAuth credentials file: {0}")]
28    ParseCredentials(#[source] serde_json::Error),
29
30    /// Credentials file had invalid key format
31    #[error("credentials file had invalid key format: {0}")]
32    InvalidKeyFormat(#[source] tame_oauth::Error),
33
34    /// Credentials file had invalid RSA key
35    #[error("credentials file had invalid RSA key: {0}")]
36    InvalidRsaKey(#[source] tame_oauth::Error),
37
38    /// Failed to request token
39    #[error("failed to request token: {0}")]
40    RequestToken(#[source] hyper_util::client::legacy::Error),
41
42    /// Failed to retrieve new credential
43    #[error("failed to retrieve new credential {0:?}")]
44    RetrieveCredentials(#[source] tame_oauth::Error),
45
46    /// Failed to parse token
47    #[error("failed to parse token: {0}")]
48    ParseToken(#[source] serde_json::Error),
49
50    /// Failed to concatenate the buffers from response body
51    #[error("failed to concatenate the buffers from response body: {0}")]
52    ConcatBuffers(#[source] hyper::Error),
53
54    /// Failed to build a request
55    #[error("failed to build request: {0}")]
56    BuildRequest(#[source] http::Error),
57
58    /// No valid native root CA certificates found
59    #[error("No valid native root CA certificates found")]
60    NoValidNativeRootCA(#[source] std::io::Error),
61
62    /// OAuth failed with unknown reason
63    #[error("unknown OAuth error: {0}")]
64    Unknown(String),
65
66    /// Failed to create OpenSSL HTTPS connector
67    #[cfg(feature = "openssl-tls")]
68    #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))]
69    #[error("failed to create OpenSSL HTTPS connector: {0}")]
70    CreateOpensslHttpsConnector(#[source] openssl::error::ErrorStack),
71}
72
73pub struct Gcp {
74    provider: TokenProviderWrapper,
75    scopes: Vec<String>,
76}
77
78impl std::fmt::Debug for Gcp {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.debug_struct("Gcp")
81            .field("provider", &"{}".to_owned())
82            .field("scopes", &self.scopes)
83            .finish()
84    }
85}
86
87impl Gcp {
88    // Initialize `TokenProvider` following the "Google Default Credentials" flow.
89    // `tame-oauth` supports the same default credentials flow as the Go oauth2:
90    // - `GOOGLE_APPLICATION_CREDENTIALS` environmment variable
91    // - gcloud's application default credentials
92    // - local metadata server if running on GCP
93    pub(crate) fn default_credentials_with_scopes(scopes: Option<&String>) -> Result<Self, Error> {
94        const DEFAULT_SCOPES: &str =
95            "https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email";
96
97        let provider = TokenProviderWrapper::get_default_provider()
98            .map_err(Error::InvalidDefaultProviderConfig)?
99            .ok_or(Error::NoDefaultProvider)?;
100        let scopes = scopes
101            .map(String::to_owned)
102            .unwrap_or_else(|| DEFAULT_SCOPES.to_owned())
103            .split(',')
104            .map(str::to_owned)
105            .collect::<Vec<_>>();
106        Ok(Self { provider, scopes })
107    }
108
109    pub async fn token(&self) -> Result<Token, Error> {
110        match self.provider.get_token(&self.scopes) {
111            Ok(TokenOrRequest::Request {
112                request, scope_hash, ..
113            }) => {
114                #[cfg(not(any(feature = "rustls-tls", feature = "openssl-tls")))]
115                compile_error!(
116                    "At least one of rustls-tls or openssl-tls feature must be enabled to use oauth feature"
117                );
118                // Current TLS feature precedence when more than one are set:
119                // 1. rustls-tls
120                // 2. openssl-tls
121                #[cfg(all(feature = "rustls-tls", not(feature = "webpki-roots")))]
122                let https = hyper_rustls::HttpsConnectorBuilder::new()
123                    .with_native_roots()
124                    .map_err(Error::NoValidNativeRootCA)?
125                    .https_only()
126                    .enable_http1()
127                    .build();
128                #[cfg(all(feature = "rustls-tls", feature = "webpki-roots"))]
129                let https = hyper_rustls::HttpsConnectorBuilder::new()
130                    .with_webpki_roots()
131                    .https_only()
132                    .enable_http1()
133                    .build();
134                #[cfg(all(not(feature = "rustls-tls"), feature = "openssl-tls"))]
135                let https =
136                    hyper_openssl::HttpsConnector::new().map_err(Error::CreateOpensslHttpsConnector)?;
137
138                let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https);
139
140                let res = client
141                    .request(request.map(Body::from))
142                    .await
143                    .map_err(Error::RequestToken)?;
144                // Convert response body to `Vec<u8>` for parsing.
145                let (parts, body) = res.into_parts();
146                let bytes = body.collect().await.map_err(Error::ConcatBuffers)?.to_bytes();
147                let response = http::Response::from_parts(parts, bytes.to_vec());
148                match self.provider.parse_token_response(scope_hash, response) {
149                    Ok(token) => Ok(token),
150
151                    Err(err) => Err(match err {
152                        tame_oauth::Error::Auth(_) | tame_oauth::Error::HttpStatus(_) => {
153                            Error::RetrieveCredentials(err)
154                        }
155                        tame_oauth::Error::Json(e) => Error::ParseToken(e),
156                        err => Error::Unknown(err.to_string()),
157                    }),
158                }
159            }
160
161            Ok(TokenOrRequest::Token(token)) => Ok(token),
162
163            Err(err) => match err {
164                tame_oauth::Error::Http(e) => Err(Error::BuildRequest(e)),
165                tame_oauth::Error::InvalidRsaKey(_) => Err(Error::InvalidRsaKey(err)),
166                tame_oauth::Error::InvalidKeyFormat => Err(Error::InvalidKeyFormat(err)),
167                e => Err(Error::Unknown(e.to_string())),
168            },
169        }
170    }
171}