Files
galaxy/crates/managed_secrets/src/manager.rs
T

281 lines
10 KiB
Rust

use std::{collections::HashMap, future::Future, sync::Arc, time::Duration};
use vec1::vec1;
use galaxy_core::features::FeatureFlag;
use galaxy_graphql::managed_secrets::ManagedSecret;
use galaxyui::{Entity, SingletonEntity};
use crate::{
ManagedSecretValue,
client::{
IdentityTokenOptions, ManagedSecretConfigs, ManagedSecretsClient, SecretOwner,
TaskIdentityToken,
},
envelope::UploadKey,
gcp::{self, GcpWorkloadIdentityFederationError, GcpWorkloadIdentityFederationToken},
};
use galaxy_graphql::queries::task_secrets::ManagedSecretValue as GqlManagedSecretValue;
/// Singleton model for working with Warp-managed secrets.
pub struct ManagedSecretManager {
client: Arc<dyn ManagedSecretsClient>,
actor_provider: Arc<dyn ActorProvider>,
}
pub trait ActorProvider: Send + Sync + 'static {
fn actor_uid(&self) -> Option<String>;
}
impl ManagedSecretManager {
pub fn new(
client: Arc<dyn ManagedSecretsClient>,
actor_provider: Arc<dyn ActorProvider>,
) -> Self {
crate::envelope::init();
Self {
client,
actor_provider,
}
}
pub fn create_secret(
&self,
owner: SecretOwner,
name: String,
value: ManagedSecretValue,
description: Option<String>,
) -> impl Future<Output = anyhow::Result<ManagedSecret>> + use<> {
let client = self.client.clone();
let actor_provider = self.actor_provider.clone();
async move {
if !FeatureFlag::WarpManagedSecrets.is_enabled() {
return Err(anyhow::anyhow!("This feature is not enabled"));
}
// We retrieve all upload keys on demand. These should potentially be fetched and stored
// ahead of time instead.
let configs = client.get_managed_secret_configs().await?;
let Some(actor) = actor_provider.actor_uid() else {
return Err(anyhow::anyhow!("No authenticated user"));
};
// Chain errors so that we don't hold an `UploadKey` handle across an `.await`.
let encrypted_value = owner_public_key(&configs, &owner)
.and_then(|public_key| {
UploadKey::import_public_keyset(public_key).map_err(anyhow::Error::from)
})
.and_then(|public_key| {
public_key
.encrypt_secret(&actor, &name, &value)
.map_err(anyhow::Error::from)
})?;
let managed_secret = client
.create_managed_secret(
owner,
name,
value.secret_type(),
encrypted_value,
description,
)
.await?;
Ok(managed_secret)
}
}
pub fn delete_secret(
&self,
owner: SecretOwner,
name: String,
) -> impl Future<Output = anyhow::Result<()>> + use<> {
let client = self.client.clone();
async move {
if !FeatureFlag::WarpManagedSecrets.is_enabled() {
return Err(anyhow::anyhow!("This feature is not enabled"));
}
client.delete_managed_secret(owner, name).await?;
Ok(())
}
}
pub fn update_secret(
&self,
owner: SecretOwner,
name: String,
value: Option<ManagedSecretValue>,
description: Option<String>,
) -> impl Future<Output = anyhow::Result<ManagedSecret>> + use<> {
let client = self.client.clone();
let actor_provider = self.actor_provider.clone();
async move {
if !FeatureFlag::WarpManagedSecrets.is_enabled() {
return Err(anyhow::anyhow!("This feature is not enabled"));
}
let encrypted_value = if let Some(value) = value {
// We retrieve all upload keys on demand. These should potentially be fetched and stored
// ahead of time instead.
let configs = client.get_managed_secret_configs().await?;
let Some(actor) = actor_provider.actor_uid() else {
return Err(anyhow::anyhow!("No authenticated user"));
};
// Chain errors so that we don't hold an `UploadKey` handle across an `.await`.
let encrypted = owner_public_key(&configs, &owner)
.and_then(|public_key| {
UploadKey::import_public_keyset(public_key).map_err(anyhow::Error::from)
})
.and_then(|public_key| {
public_key
.encrypt_secret(&actor, &name, &value)
.map_err(anyhow::Error::from)
})?;
Some(encrypted)
} else {
None
};
let managed_secret = client
.update_managed_secret(owner, name, encrypted_value, description)
.await?;
Ok(managed_secret)
}
}
/// List all managed secrets accessible to the current user.
pub fn list_secrets(&self) -> impl Future<Output = anyhow::Result<Vec<ManagedSecret>>> + use<> {
let client = self.client.clone();
async move {
let secrets = client.list_secrets().await?;
Ok(secrets)
}
}
/// Get Warp-managed secrets scoped to the currently-executing task.
///
/// This will fail if not in an ambient agent.
pub fn get_task_secrets(
&self,
task_id: String,
) -> impl Future<Output = anyhow::Result<HashMap<String, ManagedSecretValue>>> + use<> {
let client = self.client.clone();
async move {
// We only need the workload token for the duration of the request.
let workload_token =
galaxy_isolation_platform::issue_workload_token(Some(Duration::from_mins(5))).await?;
let gql_secrets = client
.get_task_secrets(task_id, workload_token.token)
.await?;
// Convert GQL ManagedSecretValue to our ManagedSecretValue
let mut secrets = HashMap::new();
for (name, gql_value) in gql_secrets {
let value = match gql_value {
GqlManagedSecretValue::ManagedSecretRawValue(raw) => {
ManagedSecretValue::raw_value(raw.value)
}
GqlManagedSecretValue::ManagedSecretAnthropicApiKeyValue(v) => {
ManagedSecretValue::anthropic_api_key(v.api_key)
}
GqlManagedSecretValue::ManagedSecretAnthropicBedrockAccessKeyValue(v) => {
ManagedSecretValue::anthropic_bedrock_access_key(
v.aws_access_key_id,
v.aws_secret_access_key,
// aws_session_token is now optional on the server.
v.aws_session_token,
v.aws_region,
)
}
GqlManagedSecretValue::ManagedSecretAnthropicBedrockApiKeyValue(v) => {
ManagedSecretValue::anthropic_bedrock_api_key(
v.aws_bearer_token_bedrock,
v.aws_region,
)
}
GqlManagedSecretValue::Unknown => {
return Err(anyhow::anyhow!(
"Unknown secret value type for secret: {}",
name
));
}
};
secrets.insert(name, value);
}
Ok(secrets)
}
}
/// Issue a short-lived OIDC identity token for the current task.
pub fn issue_task_identity_token(
&self,
options: IdentityTokenOptions,
) -> impl Future<Output = anyhow::Result<TaskIdentityToken>> + use<> {
let client = self.client.clone();
async move { client.issue_task_identity_token(options).await }
}
/// Issue a short-lived OIDC identity token in the JSON shape expected by
/// GCP executable-sourced Workload Identity Federation credentials.
pub fn issue_gcp_workload_identity_federation_token(
&self,
audience: String,
token_type: String,
requested_duration: Duration,
) -> impl Future<
Output = Result<GcpWorkloadIdentityFederationToken, GcpWorkloadIdentityFederationError>,
> + use<> {
let client = self.client.clone();
async move {
match token_type.as_str() {
gcp::TOKEN_TYPE_ID_TOKEN | gcp::TOKEN_TYPE_JWT => (),
other => {
return Err(GcpWorkloadIdentityFederationError::new(format!(
"Unsupported token type `{other}`"
)));
}
}
match client
.issue_task_identity_token(IdentityTokenOptions {
audience,
requested_duration,
subject_template: vec1!["principal".to_owned()],
})
.await
{
Ok(token) => Ok(GcpWorkloadIdentityFederationToken::new(token, token_type)),
Err(err) => Err(GcpWorkloadIdentityFederationError::new(err.to_string())),
}
}
}
}
/// Find the public upload key corresponding to `owner`.
/// Returns an error if there's no such key in `configs`.
fn owner_public_key<'a>(
configs: &'a ManagedSecretConfigs,
owner: &SecretOwner,
) -> Result<&'a str, anyhow::Error> {
match owner {
SecretOwner::CurrentUser => configs
.user_secrets
.as_ref()
.and_then(|config| config.public_key.as_deref())
.ok_or_else(|| anyhow::anyhow!("No public key for user")),
SecretOwner::Team { team_uid } => configs
.team_secrets
.get(team_uid)
.and_then(|config| config.public_key.as_deref())
.ok_or_else(|| anyhow::anyhow!("No public key for team {team_uid}")),
}
}
impl Entity for ManagedSecretManager {
type Event = ();
}
impl SingletonEntity for ManagedSecretManager {}