107 lines
3.4 KiB
Rust
107 lines
3.4 KiB
Rust
use std::time::Duration;
|
|
use std::{env, fs};
|
|
|
|
use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine as _};
|
|
use chrono::{DateTime, Utc};
|
|
use command::r#async::Command;
|
|
use galaxy_core::channel::ChannelState;
|
|
|
|
use crate::{IsolationPlatformError, WorkloadToken};
|
|
|
|
/// Detect whether or not we are running in a Namespace instance.
|
|
pub fn is_in_namespace_instance() -> bool {
|
|
// For Namespace, match their CLI's logic for detecting a token:
|
|
// https://github.com/namespacelabs/integrations/blob/08d0acd17ce05f8486ec8da329066dd6a12572a0/auth/token.go#L116-L131
|
|
env::var("NSC_TOKEN_FILE").is_ok() || fs::exists("/var/run/nsc/token.json").is_ok_and(|v| v)
|
|
}
|
|
|
|
/// Issue a Namespace workload identity token.
|
|
pub async fn issue_workload_token(
|
|
duration: Option<Duration>,
|
|
) -> Result<WorkloadToken, IsolationPlatformError> {
|
|
let mut nsc_command = Command::new("nsc");
|
|
nsc_command
|
|
.arg("auth")
|
|
.arg("issue-id-token")
|
|
.arg("--audience")
|
|
.arg(&*ChannelState::workload_audience_url())
|
|
.arg("--output")
|
|
.arg("json");
|
|
|
|
if let Some(duration) = duration {
|
|
nsc_command
|
|
.arg("--duration")
|
|
.arg(format!("{}ns", duration.as_nanos()));
|
|
}
|
|
|
|
let output =
|
|
nsc_command
|
|
.output()
|
|
.await
|
|
.map_err(|err| IsolationPlatformError::CommandUnavailable {
|
|
command: "nsc".to_owned(),
|
|
source: err,
|
|
})?;
|
|
|
|
if !output.status.success() {
|
|
log::warn!(
|
|
"`nsc` command failed with status {}: {}",
|
|
output.status,
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
return Err(IsolationPlatformError::CommandFailed {
|
|
command: "nsc".to_owned(),
|
|
status: output.status,
|
|
});
|
|
}
|
|
|
|
/// JSON output from `nsc auth issue-id-token`.
|
|
#[derive(serde::Deserialize)]
|
|
struct NscTokenOutput {
|
|
id_token: String,
|
|
}
|
|
|
|
let token_output = serde_json::from_slice::<NscTokenOutput>(&output.stdout)
|
|
.map_err(|_| anyhow::anyhow!("Unexpected output from `nsc auth issue-id-token`"))?;
|
|
|
|
// Namespace ID tokens are JWTs.
|
|
let expires_at = parse_jwt_expiration(&token_output.id_token)?;
|
|
|
|
Ok(WorkloadToken {
|
|
token: token_output.id_token,
|
|
expires_at: Some(expires_at),
|
|
})
|
|
}
|
|
|
|
/// Parse the expiration time from a JWT token.
|
|
///
|
|
/// JWTs have three base64url-encoded parts separated by dots: header.payload.signature.
|
|
/// The payload contains an `exp` claim with the Unix timestamp of expiration.
|
|
fn parse_jwt_expiration(token: &str) -> Result<DateTime<Utc>, IsolationPlatformError> {
|
|
let parts: Vec<&str> = token.split('.').collect();
|
|
if parts.len() != 3 {
|
|
return Err(
|
|
anyhow::anyhow!("Invalid JWT format: expected 3 parts, got {}", parts.len()).into(),
|
|
);
|
|
}
|
|
|
|
let payload_bytes = BASE64_URL_SAFE_NO_PAD
|
|
.decode(parts[1])
|
|
.map_err(|e| anyhow::anyhow!("Failed to decode JWT payload: {e}"))?;
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct JwtPayload {
|
|
exp: i64,
|
|
}
|
|
|
|
let payload: JwtPayload = serde_json::from_slice(&payload_bytes)
|
|
.map_err(|e| anyhow::anyhow!("Failed to parse JWT payload: {e}"))?;
|
|
|
|
DateTime::from_timestamp(payload.exp, 0)
|
|
.ok_or_else(|| anyhow::anyhow!("Invalid exp timestamp in JWT: {}", payload.exp).into())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "namespace_tests.rs"]
|
|
mod tests;
|