diff --git a/README.md b/README.md index 85fd3e7..c4b6953 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ $ TOKEN=$(cat DEFAULT.token) \ ``` You should see `X-Factor-Client-Id: DEFAULT` in the response from the server. -Note that in this case we are generating an oidc token an validating it +Note that in this case we are generating an oidc token and validating it locally. ## Governance and Code of Conduct @@ -171,3 +171,16 @@ the environment. Identity data looks like: If a matching token is supplied, then the header `X-Factor-Client-Id` will be set to the value of the matching client id. To reject, requests that don't match, use the flag `--reject-unknown` + +`factor issuer` + +Prints out the issuer and subject for the application. If factor is already +running it will dynamically print out the current issuer, otherwise loads the +identity provider to determine the issuer. + +The output will be something like: + +``` +issuer=http://localhost:5000 +subject=local +``` diff --git a/src/identity/mod.rs b/src/identity/mod.rs index 7e0f470..329a39c 100644 --- a/src/identity/mod.rs +++ b/src/identity/mod.rs @@ -9,7 +9,7 @@ use log::{error, info, trace, warn}; pub use providers::*; use regex::Regex; use serde::{Deserialize, Serialize}; -use tokio::{sync::watch, time::interval}; +use tokio::{fs::write, sync::watch, time::interval}; use super::{dirs, env, server::Service}; @@ -65,7 +65,10 @@ pub trait IdentityProvider: Send + Sync { async fn ensure_audience(&self, audience: &str) -> Result<()>; async fn get_sub(&self) -> Result; async fn get_token(&self, audience: &str) -> Result; - async fn get_iss_and_jwks(&self) -> Result>; + async fn get_iss(&self) -> Result; + async fn get_jwks(&self) -> Result> { + Ok(None) + } } pub struct IdentitySyncService { @@ -113,9 +116,8 @@ impl IdentitySyncService { } async fn write_issuer(&self) -> Result<()> { - if let Some((issuer, _)) = self.provider.get_iss_and_jwks().await? { - env::set_var_file("ISSUER", &issuer, &self.issuer_path)?; - } + let issuer = self.provider.get_iss().await?; + write(&self.issuer_path, issuer.as_bytes()).await?; Ok(()) } } diff --git a/src/identity/providers/auth0.rs b/src/identity/providers/auth0.rs index 0994f5d..bd124e8 100644 --- a/src/identity/providers/auth0.rs +++ b/src/identity/providers/auth0.rs @@ -85,9 +85,8 @@ struct Auth0Response { #[async_trait] impl IdentityProvider for Provider { - async fn get_iss_and_jwks(&self) -> Result> { - // No jwks management - Ok(None) + async fn get_iss(&self) -> Result { + self.config.issuer.clone().context("Issuer not configured") } async fn configure_app_identity(&self, name: &str) -> Result { diff --git a/src/identity/providers/dummy.rs b/src/identity/providers/dummy.rs index cb7ddb0..aa9eb7c 100644 --- a/src/identity/providers/dummy.rs +++ b/src/identity/providers/dummy.rs @@ -37,9 +37,8 @@ impl Provider { #[async_trait] impl IdentityProvider for Provider { - async fn get_iss_and_jwks(&self) -> Result> { - // No jwks management - Ok(None) + async fn get_iss(&self) -> Result { + Ok("dummy-issuer".to_string()) } async fn configure_app_identity(&self, name: &str) -> Result { let mut config = self.config.clone(); diff --git a/src/identity/providers/k8s.rs b/src/identity/providers/k8s.rs index 3abf844..8707fcc 100644 --- a/src/identity/providers/k8s.rs +++ b/src/identity/providers/k8s.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use anyhow::{Context, Result}; use async_trait::async_trait; use http::uri::Uri; @@ -11,9 +13,10 @@ use kube::{ Client, }; use log::trace; +use reqwest::Url; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; -use tokio::sync::OnceCell; +use tokio::sync::{OnceCell, RwLock}; use crate::identity::{IdentityProvider, ProviderConfig}; @@ -21,8 +24,7 @@ use crate::identity::{IdentityProvider, ProviderConfig}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { #[serde_as(as = "Option")] - pub cluster_url: Option, - pub namespace: Option, + pub default_namespace: Option, pub service_account_name: Option, pub kubeconfig_path: Option, } @@ -30,6 +32,7 @@ pub struct Config { #[derive(Clone)] pub struct Provider { config: Config, + issuer: Arc>>, client: OnceCell, } @@ -41,6 +44,7 @@ impl Provider { pub fn new(config: Config) -> Result { Ok(Self { config, + issuer: Arc::new(RwLock::new(None)), client: OnceCell::new(), }) } @@ -87,13 +91,68 @@ impl Provider { }) .await } + + /// # Errors + /// + /// This method returns an [`anyhow::Error`] if: + /// + /// - Configuration error in `self.config.kubeconfig_path`: + /// - the config file does not exist + /// - the config file is not valid YAML + /// - the config file is missing a context + /// - see [`kube::config::Kubeconfig::read_from`] + /// - see [`kube::config::Config::from_custom_kubeconfig`] + /// + /// - The cluster URL cannot be inferred from the Kubernetes configuration. + /// + /// - See [`kube::Config::infer`] + pub async fn get_cluster_url(&self) -> Result { + if let Some(path) = &self.config.kubeconfig_path { + let kube_config = kube::config::Kubeconfig::read_from(path) + .context("Failed to read kubeconfig from path")?; + let client_config = kube::config::Config::from_custom_kubeconfig( + kube_config, + &KubeConfigOptions::default(), + ) + .await + .context("Failed to create config from kubeconfig")?; + Ok(client_config.cluster_url) + } else { + let config = kube::Config::infer() + .await + .context("Failed to infer Kubernetes config")?; + Ok(config.cluster_url) + } + } +} + +#[derive(Debug, Deserialize)] +struct OidcDiscovery { + issuer: String, + // Add other fields if needed } #[async_trait] impl IdentityProvider for Provider { - async fn get_iss_and_jwks(&self) -> Result> { - // No jwks management - Ok(None) + async fn get_iss(&self) -> Result { + if let Some(issuer) = &*self.issuer.read().await { + return Ok(issuer.clone()); + } + + let cluster_url = self.get_cluster_url().await?; + + // Convert http::Uri to url::Url + let cluster_url = + Url::parse(&cluster_url.to_string()).context("Failed to parse cluster URL")?; + + let discovery_url = cluster_url.join(".well-known/openid-configuration")?; + let response = reqwest::get(discovery_url).await?; + let oidc_discovery: OidcDiscovery = response.json().await?; + + *self.issuer.write().await = Some(oidc_discovery.issuer.clone()); + + // Return the issuer URL + Ok(oidc_discovery.issuer) } async fn configure_app_identity(&self, name: &str) -> Result { @@ -120,6 +179,7 @@ impl IdentityProvider for Provider { // Create new config with service account name let mut config = self.config.clone(); + config.default_namespace = Some(client.default_namespace().to_string()); config.service_account_name = Some(sa_name); Ok(ProviderConfig::k8s(config)) @@ -171,7 +231,7 @@ impl IdentityProvider for Provider { .context("Service account name not configured")?; let namespace = self .config - .namespace + .default_namespace .as_ref() .context("Namespace not configured")?; Ok(format!("system:serviceaccount:{namespace}:{sa_name}")) diff --git a/src/identity/providers/local.rs b/src/identity/providers/local.rs index b979f0d..e8e54f6 100644 --- a/src/identity/providers/local.rs +++ b/src/identity/providers/local.rs @@ -132,11 +132,13 @@ struct Claims { #[async_trait] impl IdentityProvider for Provider { - async fn get_iss_and_jwks(&self) -> Result> { + async fn get_iss(&self) -> Result { // if iss is still an env var, expand it now - let iss = env::expand(&self.config.iss)?; + env::expand(&self.config.iss) + } + async fn get_jwks(&self) -> Result> { let json = serde_json::to_string_pretty(&self.jwks)?; - Ok(Some((iss, json))) + Ok(Some(json)) } async fn configure_app_identity(&self, name: &str) -> Result { diff --git a/src/main.rs b/src/main.rs index f6c9b91..0b08148 100644 --- a/src/main.rs +++ b/src/main.rs @@ -310,11 +310,8 @@ fn handle_issuer(app_config: &AppConfig) -> Result<(), anyhow::Error> { let issuer = if let Ok(iss) = identity::get_stored_issuer() { iss } else { - info!("Could not get stored issuer, falling back to generating a token"); - // Generate a token with a dummy audience to extract issuer - let token = rt.block_on(provider.get_token("dummy-audience"))?; - let claims = identity::get_claims(&token)?; - claims.iss + info!("Could not get stored issuer, falling back to provider"); + rt.block_on(provider.get_iss())? }; // Get subject directly diff --git a/src/proxy.rs b/src/proxy.rs index cc9e280..af25c5c 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -39,12 +39,8 @@ pub struct AuthProxy { } impl AuthProxy { - async fn handle_well_known( - &self, - session: &mut Session, - iss: String, - jwks: String, - ) -> anyhow::Result<()> { + async fn handle_well_known(&self, session: &mut Session, jwks: String) -> anyhow::Result<()> { + let iss = self.provider.get_iss().await?; let path = session.req_header().uri.path(); let resp_str = if path.starts_with("/.well-known/openid-configuration") { // Generate the OIDC discovery response @@ -180,8 +176,8 @@ impl ProxyHttp for AuthProxy { async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result { if session.req_header().uri.path().starts_with("/.well-known/") { - if let Ok(Some((iss, jwks))) = self.provider.get_iss_and_jwks().await { - if let Err(e) = self.handle_well_known(session, iss, jwks).await { + if let Ok(Some(jwks)) = self.provider.get_jwks().await { + if let Err(e) = self.handle_well_known(session, jwks).await { info!("Failed to handle well_known {e:?}"); let _ = session.respond_error(500).await; return Ok(true);