diff --git a/examples/custom_client.rs b/examples/custom_client.rs index 0b61a8810..afd24bc87 100644 --- a/examples/custom_client.rs +++ b/examples/custom_client.rs @@ -12,13 +12,13 @@ async fn main() -> anyhow::Result<()> { let config = Config::infer().await?; - let https = config.rustls_https_connector()?; + let https = config.rustls_https_connector(None)?; let service = tower::ServiceBuilder::new() .layer(config.base_uri_layer()) .option_layer(config.auth_layer()?) .map_err(BoxError::from) .service(hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https)); - let client = Client::new(service, config.default_namespace); + let client = Client::new(service, config.default_namespace, None); let pods: Api = Api::default_namespaced(client); for p in pods.list(&Default::default()).await? { diff --git a/examples/custom_client_trace.rs b/examples/custom_client_trace.rs index 25cb1aa6c..7e198ae40 100644 --- a/examples/custom_client_trace.rs +++ b/examples/custom_client_trace.rs @@ -18,7 +18,7 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let config = Config::infer().await?; - let https = config.rustls_https_connector()?; + let https = config.rustls_https_connector(None)?; let service = ServiceBuilder::new() .layer(config.base_uri_layer()) // showcase rate limiting; max 10rps, and 4 concurrent @@ -57,7 +57,7 @@ async fn main() -> anyhow::Result<()> { .map_err(BoxError::from) .service(hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https)); - let client = Client::new(service, config.default_namespace); + let client = Client::new(service, config.default_namespace, None); let pods: Api = Api::default_namespaced(client); for p in pods.list(&Default::default()).await? { diff --git a/kube-client/src/api/mod.rs b/kube-client/src/api/mod.rs index 020d95337..15fb136c2 100644 --- a/kube-client/src/api/mod.rs +++ b/kube-client/src/api/mod.rs @@ -256,7 +256,7 @@ mod test { #[tokio::test] async fn scopes_should_allow_correct_interface() { let (mock_service, _handle) = mock::pair::, Response>(); - let client = Client::new(mock_service, "default"); + let client = Client::new(mock_service, "default", None); let _: Api = Api::all(client.clone()); let _: Api = Api::default_namespaced(client.clone()); diff --git a/kube-client/src/client/auth/mod.rs b/kube-client/src/client/auth/mod.rs index f051bba45..77cc4f725 100644 --- a/kube-client/src/client/auth/mod.rs +++ b/kube-client/src/client/auth/mod.rs @@ -115,7 +115,7 @@ pub(crate) enum Auth { Basic(String, SecretString), Bearer(SecretString), RefreshableToken(RefreshableToken), - Certificate(String, SecretString), + Certificate(String, SecretString, Option>), } // Token file reference. Reloads at least once per minute. @@ -227,7 +227,7 @@ impl RefreshableToken { if Utc::now() + SIXTY_SEC >= locked_data.1 { // TODO Improve refreshing exec to avoid `Auth::try_from` match Auth::try_from(&locked_data.2)? { - Auth::None | Auth::Basic(_, _) | Auth::Bearer(_) | Auth::Certificate(_, _) => { + Auth::None | Auth::Basic(_, _) | Auth::Bearer(_) | Auth::Certificate(_, _, _) => { return Err(Error::UnrefreshableTokenResponse); } @@ -350,16 +350,21 @@ impl TryFrom<&AuthInfo> for Auth { if let Some(exec) = &auth_info.exec { let creds = auth_exec(exec)?; let status = creds.status.ok_or(Error::ExecPluginFailed)?; - if let (Some(client_certificate_data), Some(client_key_data)) = - (status.client_certificate_data, status.client_key_data) - { - return Ok(Self::Certificate(client_certificate_data, client_key_data.into())); - } let expiration = status .expiration_timestamp .map(|ts| ts.parse()) .transpose() .map_err(Error::MalformedTokenExpirationDate)?; + + if let (Some(client_certificate_data), Some(client_key_data)) = + (status.client_certificate_data, status.client_key_data) + { + return Ok(Self::Certificate( + client_certificate_data, + client_key_data.into(), + expiration, + )); + } match (status.token.map(SecretString::from), expiration) { (Some(token), Some(expire)) => Ok(Self::RefreshableToken(RefreshableToken::Exec(Arc::new( Mutex::new((token, expire, auth_info.clone())), diff --git a/kube-client/src/client/builder.rs b/kube-client/src/client/builder.rs index bb2518393..258fa57bc 100644 --- a/kube-client/src/client/builder.rs +++ b/kube-client/src/client/builder.rs @@ -1,4 +1,5 @@ use bytes::Bytes; +use chrono::{DateTime, Utc}; use http::{header::HeaderMap, Request, Response}; use hyper::{ body::Incoming, @@ -30,6 +31,7 @@ pub type DynBody = dyn http_body::Body + Send + pub struct ClientBuilder { service: Svc, default_ns: String, + valid_until: Option>, } impl ClientBuilder { @@ -37,13 +39,14 @@ impl ClientBuilder { /// /// This method is only intended for advanced use cases, most users will want to use [`ClientBuilder::try_from`] instead, /// which provides a default stack as a starting point. - pub fn new(service: Svc, default_namespace: impl Into) -> Self + pub fn new(service: Svc, default_namespace: impl Into, valid_until: Option>) -> Self where Svc: Service>, { Self { service, default_ns: default_namespace.into(), + valid_until, } } @@ -52,10 +55,12 @@ impl ClientBuilder { let Self { service: stack, default_ns, + valid_until, } = self; ClientBuilder { service: layer.layer(stack), default_ns, + valid_until, } } @@ -68,7 +73,7 @@ impl ClientBuilder { B: http_body::Body + Send + 'static, B::Error: Into, { - Client::new(self.service, self.default_ns) + Client::new(self.service, self.default_ns, self.valid_until) } } @@ -148,6 +153,9 @@ where let default_ns = config.default_namespace.clone(); let auth_layer = config.auth_layer()?; + let (exec_identity, expiration) = config.exec_identity_pem(); + let identity = exec_identity.or_else(|| config.identity_pem()); + let client: hyper_util::client::legacy::Client<_, Body> = { // Current TLS feature precedence when more than one are set: // 1. rustls-tls @@ -155,9 +163,9 @@ where // Create a custom client to use something else. // If TLS features are not enabled, http connector will be used. #[cfg(feature = "rustls-tls")] - let connector = config.rustls_https_connector_with_connector(connector)?; + let connector = config.rustls_https_connector_with_connector(connector, identity)?; #[cfg(all(not(feature = "rustls-tls"), feature = "openssl-tls"))] - let connector = config.openssl_https_connector_with_connector(connector)?; + let connector = config.openssl_https_connector_with_connector(connector, identity)?; #[cfg(all(not(feature = "rustls-tls"), not(feature = "openssl-tls")))] if config.cluster_url.scheme() == Some(&http::uri::Scheme::HTTPS) { // no tls stack situation only works with http scheme @@ -250,6 +258,7 @@ where .layer(service), ), default_ns, + expiration, )) } diff --git a/kube-client/src/client/config_ext.rs b/kube-client/src/client/config_ext.rs index 0874e0f00..e48e66698 100644 --- a/kube-client/src/client/config_ext.rs +++ b/kube-client/src/client/config_ext.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use chrono::{DateTime, Utc}; use http::{header::HeaderName, HeaderValue}; #[cfg(feature = "openssl-tls")] use hyper::rt::{Read, Write}; use hyper_util::client::legacy::connect::HttpConnector; @@ -44,7 +45,10 @@ pub trait ConfigExt: private::Sealed { /// ``` #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg(feature = "rustls-tls")] - fn rustls_https_connector(&self) -> Result>; + fn rustls_https_connector( + &self, + identity: Option>, + ) -> Result>; /// Create [`hyper_rustls::HttpsConnector`] based on config and `connector`. /// @@ -67,29 +71,9 @@ pub trait ConfigExt: private::Sealed { fn rustls_https_connector_with_connector( &self, connector: H, + identity: Option>, ) -> Result>; - /// Create [`rustls::ClientConfig`] based on config. - /// # Example - /// - /// ```rust - /// # async fn doc() -> Result<(), Box> { - /// # use hyper_util::client::legacy::connect::HttpConnector; - /// # use kube::{client::ConfigExt, Config}; - /// let config = Config::infer().await?; - /// let https = { - /// let rustls_config = std::sync::Arc::new(config.rustls_client_config()?); - /// let mut http = HttpConnector::new(); - /// http.enforce_http(false); - /// hyper_rustls::HttpsConnector::from((http, rustls_config)) - /// }; - /// # Ok(()) - /// # } - /// ``` - #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] - #[cfg(feature = "rustls-tls")] - fn rustls_client_config(&self) -> Result; - /// Create [`hyper_openssl::HttpsConnector`] based on config. /// # Example /// @@ -103,8 +87,10 @@ pub trait ConfigExt: private::Sealed { /// ``` #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))] #[cfg(feature = "openssl-tls")] - fn openssl_https_connector(&self) - -> Result>; + fn openssl_https_connector( + &self, + identity: Option>, + ) -> Result>; /// Create [`hyper_openssl::HttpsConnector`] based on config and `connector`. /// # Example @@ -125,6 +111,7 @@ pub trait ConfigExt: private::Sealed { fn openssl_https_connector_with_connector( &self, connector: H, + identity: Option>, ) -> Result> where H: tower::Service + Send, @@ -151,7 +138,10 @@ pub trait ConfigExt: private::Sealed { /// ``` #[cfg_attr(docsrs, doc(cfg(feature = "openssl-tls")))] #[cfg(feature = "openssl-tls")] - fn openssl_ssl_connector_builder(&self) -> Result; + fn openssl_ssl_connector_builder( + &self, + identity: Option>, + ) -> Result; } mod private { @@ -176,7 +166,7 @@ impl ConfigExt for Config { Auth::RefreshableToken(refreshable) => { Some(AuthLayer(Either::Right(AsyncFilterLayer::new(refreshable)))) } - Auth::Certificate(_client_certificate_data, _client_key_data) => None, + Auth::Certificate(_client_certificate_data, _client_key_data, _) => None, }) } @@ -206,33 +196,32 @@ impl ConfigExt for Config { } #[cfg(feature = "rustls-tls")] - fn rustls_client_config(&self) -> Result { - let identity = self.exec_identity_pem().or_else(|| self.identity_pem()); - tls::rustls_tls::rustls_client_config( - identity.as_deref(), - self.root_cert.as_deref(), - self.accept_invalid_certs, - ) - .map_err(Error::RustlsTls) - } - - #[cfg(feature = "rustls-tls")] - fn rustls_https_connector(&self) -> Result> { + fn rustls_https_connector( + &self, + identity: Option>, + ) -> Result> { let mut connector = HttpConnector::new(); connector.enforce_http(false); - self.rustls_https_connector_with_connector(connector) + self.rustls_https_connector_with_connector(connector, identity) } #[cfg(feature = "rustls-tls")] fn rustls_https_connector_with_connector( &self, connector: H, + identity: Option>, ) -> Result> { use hyper_rustls::FixedServerNameResolver; use crate::client::tls::rustls_tls; - let rustls_config = self.rustls_client_config()?; + let rustls_config = tls::rustls_tls::rustls_client_config( + identity.as_deref(), + self.root_cert.as_deref(), + self.accept_invalid_certs, + ) + .map_err(Error::RustlsTls)?; + let mut builder = hyper_rustls::HttpsConnectorBuilder::new() .with_tls_config(rustls_config) .https_or_http(); @@ -248,8 +237,10 @@ impl ConfigExt for Config { } #[cfg(feature = "openssl-tls")] - fn openssl_ssl_connector_builder(&self) -> Result { - let identity = self.exec_identity_pem().or_else(|| self.identity_pem()); + fn openssl_ssl_connector_builder( + &self, + identity: Option>, + ) -> Result { // TODO: pass self.tls_server_name for openssl tls::openssl_tls::ssl_connector_builder(identity.as_ref(), self.root_cert.as_ref()) .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateSslConnector(e))) @@ -258,16 +249,18 @@ impl ConfigExt for Config { #[cfg(feature = "openssl-tls")] fn openssl_https_connector( &self, + identity: Option>, ) -> Result> { let mut connector = HttpConnector::new(); connector.enforce_http(false); - self.openssl_https_connector_with_connector(connector) + self.openssl_https_connector_with_connector(connector, identity) } #[cfg(feature = "openssl-tls")] fn openssl_https_connector_with_connector( &self, connector: H, + identity: Option>, ) -> Result> where H: tower::Service + Send, @@ -277,7 +270,7 @@ impl ConfigExt for Config { { let mut https = hyper_openssl::client::legacy::HttpsConnector::with_connector( connector, - self.openssl_ssl_connector_builder()?, + self.openssl_ssl_connector_builder(identity)?, ) .map_err(|e| Error::OpensslTls(tls::openssl_tls::Error::CreateHttpsConnector(e)))?; if self.accept_invalid_certs { @@ -291,22 +284,30 @@ impl ConfigExt for Config { } impl Config { + /// Retrieves an identity when an exec plugin returns a client certificate and key instead of a token. + /// + /// This is necessary to check on TLS configuration vs tokens which can be added in as an AuthLayer. + /// + /// # Returns + /// + /// A tuple containing an optional vector of bytes representing the identity and an optional expiration date. + // This is necessary to retrieve an identity when an exec plugin // returns a client certificate and key instead of a token. // This has be to be checked on TLS configuration vs tokens // which can be added in as an AuthLayer. - fn exec_identity_pem(&self) -> Option> { + pub fn exec_identity_pem(&self) -> (Option>, Option>) { match Auth::try_from(&self.auth_info) { - Ok(Auth::Certificate(client_certificate_data, client_key_data)) => { + Ok(Auth::Certificate(client_certificate_data, client_key_data, expiratiom)) => { const NEW_LINE: u8 = b'\n'; let mut buffer = client_key_data.expose_secret().as_bytes().to_vec(); buffer.push(NEW_LINE); buffer.extend_from_slice(client_certificate_data.as_bytes()); buffer.push(NEW_LINE); - Some(buffer) + (Some(buffer), expiratiom) } - _ => None, + _ => (None, None), } } } diff --git a/kube-client/src/client/mod.rs b/kube-client/src/client/mod.rs index cd6c9ac9e..edd7648f1 100644 --- a/kube-client/src/client/mod.rs +++ b/kube-client/src/client/mod.rs @@ -7,6 +7,7 @@ //! //! The [`Client`] can also be used with [`Discovery`](crate::Discovery) to dynamically //! retrieve the resources served by the kubernetes API. +use chrono::{DateTime, Utc}; use either::{Either, Left, Right}; use futures::{future::BoxFuture, AsyncBufRead, StreamExt, TryStream, TryStreamExt}; use http::{self, Request, Response}; @@ -78,6 +79,7 @@ pub struct Client { // - `BoxFuture` for dynamic response future type inner: Buffer, BoxFuture<'static, Result, BoxError>>>, default_ns: String, + valid_until: Option>, } /// Constructors and low-level api interfaces. @@ -115,7 +117,7 @@ impl Client { /// # Ok(()) /// # } /// ``` - pub fn new(service: S, default_namespace: T) -> Self + pub fn new(service: S, default_namespace: T, valid_until: Option>) -> Self where S: Service, Response = Response> + Send + 'static, S::Future: Send + 'static, @@ -131,6 +133,7 @@ impl Client { Self { inner: Buffer::new(BoxService::new(service), 1024), default_ns: default_namespace.into(), + valid_until, } } @@ -164,6 +167,14 @@ impl Client { &self.default_ns } + /// Get the time when the client will expire + /// + /// This will only be set if the client was created with client credentials that has an expiry time. + /// You may ignore this if you are using an authentication method that does not expire, or that can be refreshed automatically. + pub fn valid_until(&self) -> &Option> { + &self.valid_until + } + /// Perform a raw HTTP request against the API and return the raw response back. /// This method can be used to get raw access to the API which may be used to, for example, /// create a proxy server or application-level gateway between localhost and the API server. @@ -504,7 +515,7 @@ mod tests { #[tokio::test] async fn test_default_ns() { let (mock_service, _) = mock::pair::, Response>(); - let client = Client::new(mock_service, "test-namespace"); + let client = Client::new(mock_service, "test-namespace", None); assert_eq!(client.default_namespace(), "test-namespace"); } @@ -536,7 +547,7 @@ mod tests { ); }); - let pods: Api = Api::default_namespaced(Client::new(mock_service, "default")); + let pods: Api = Api::default_namespaced(Client::new(mock_service, "default", None)); let pod = pods.get("test").await.unwrap(); assert_eq!(pod.metadata.annotations.unwrap().get("kube-rs").unwrap(), "test"); spawned.await.unwrap(); diff --git a/kube-client/src/lib.rs b/kube-client/src/lib.rs index f32875c0e..dc1f60b68 100644 --- a/kube-client/src/lib.rs +++ b/kube-client/src/lib.rs @@ -153,11 +153,11 @@ mod test { use hyper_util::rt::TokioExecutor; let config = Config::infer().await?; - let https = config.rustls_https_connector()?; + let https = config.rustls_https_connector(None)?; let service = ServiceBuilder::new() .layer(config.base_uri_layer()) .service(hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(https)); - let client = Client::new(service, config.default_namespace); + let client = Client::new(service, config.default_namespace, None); let pods: Api = Api::default_namespaced(client); pods.list(&Default::default()).await?; Ok(()) diff --git a/kube/src/mock_tests.rs b/kube/src/mock_tests.rs index c651a885a..c74c53bd9 100644 --- a/kube/src/mock_tests.rs +++ b/kube/src/mock_tests.rs @@ -154,6 +154,6 @@ impl ApiServerVerifier { // Create a test context with a mocked kube client fn testcontext() -> (Client, ApiServerVerifier) { let (mock_service, handle) = tower_test::mock::pair::, Response>(); - let mock_client = Client::new(mock_service, "default"); + let mock_client = Client::new(mock_service, "default", None); (mock_client, ApiServerVerifier(handle)) }