use std::collections::HashMap; use anyhow::{Context, Result, anyhow}; use async_trait::async_trait; use cynic::{MutationBuilder, QueryBuilder}; use warp_graphql::managed_secrets::{ManagedSecret, ManagedSecretType}; use warp_graphql::mutations::create_managed_secret::{ CreateManagedSecret, CreateManagedSecretInput, CreateManagedSecretResult, CreateManagedSecretVariables, }; use warp_graphql::mutations::delete_managed_secret::{ DeleteManagedSecret, DeleteManagedSecretInput, DeleteManagedSecretResult, DeleteManagedSecretVariables, }; use warp_graphql::mutations::issue_task_identity_token::{ IssueTaskIdentityToken, IssueTaskIdentityTokenInput, IssueTaskIdentityTokenResult, IssueTaskIdentityTokenVariables, }; use warp_graphql::mutations::update_managed_secret::{ UpdateManagedSecret, UpdateManagedSecretInput, UpdateManagedSecretResult, UpdateManagedSecretVariables, }; use warp_graphql::object_permissions::{Owner, OwnerType}; use warp_graphql::queries::list_harness_auth_secrets::{ ListHarnessAuthSecrets, ListHarnessAuthSecretsInput, ListHarnessAuthSecretsVariables, }; use warp_graphql::queries::list_managed_secrets::{ ListManagedSecrets, ListManagedSecretsVariables, ManagedSecretsInput, ManagedSecretsResult, }; use warp_graphql::queries::managed_secret_config::{ GetManagedSecretConfig, GetManagedSecretConfigVariables, UserResult, }; use warp_graphql::queries::task_secrets::{ ManagedSecretValue, TaskSecrets, TaskSecretsInput, TaskSecretsResult, TaskSecretsVariables, }; pub use warp_managed_secrets::client::{ManagedSecretConfigs, ManagedSecretsClient}; use warp_managed_secrets::client::{SecretOwner, TaskIdentityToken}; use super::ServerApi; use crate::server::graphql::{get_request_context, get_user_facing_error_message}; use crate::server::team_scope::RequestTeamScope; pub(crate) type AppManagedSecretManager = warp_managed_secrets::ManagedSecretManager; pub(crate) type AppManagedSecretsClient = dyn ManagedSecretsClient; #[cfg_attr(not(target_family = "wasm"), async_trait)] #[cfg_attr(target_family = "wasm", async_trait(?Send))] impl ManagedSecretsClient for ServerApi { type RequestScope = RequestTeamScope; async fn get_managed_secret_configs( &self, request_scope: &Self::RequestScope, ) -> Result { let variables = GetManagedSecretConfigVariables { request_context: get_request_context(), }; let operation = GetManagedSecretConfig::build(variables); let response = self .send_graphql_request_for_team(operation, *request_scope) .await?; match response.user { UserResult::UserOutput(output) => { let mut team_configs = HashMap::new(); for workspace in output.user.workspaces { for team in workspace.teams { if let Some(config) = team.managed_secrets { // DO NOT inline the `insert` call into the `debug_assert!` macro. It will get compiled out in release builds. let prior_config = team_configs.insert(team.uid.into_inner(), config); debug_assert!( prior_config.is_none(), "Duplicate team UID returned from server" ); } } } Ok(ManagedSecretConfigs { user_secrets: output.user.managed_secrets, team_secrets: team_configs, }) } UserResult::UserFacingError(error) => { Err(anyhow!(get_user_facing_error_message(error))) } UserResult::Unknown => Err(anyhow!( "Unknown error while getting managed secret configs" )), } } async fn create_managed_secret( &self, request_scope: &Self::RequestScope, owner: SecretOwner, name: String, secret_type: ManagedSecretType, encrypted_value: String, description: Option, ) -> Result { let graphql_owner = match owner { SecretOwner::CurrentUser => Owner { type_: OwnerType::User, uid: None, }, SecretOwner::Team { team_uid } => Owner { type_: OwnerType::Team, uid: Some(cynic::Id::new(team_uid)), }, }; let variables = CreateManagedSecretVariables { input: CreateManagedSecretInput { description, encrypted_value, name, owner: graphql_owner, type_: secret_type, }, request_context: get_request_context(), }; let operation = CreateManagedSecret::build(variables); let response = self .send_graphql_request_for_team(operation, *request_scope) .await?; match response.create_managed_secret { CreateManagedSecretResult::CreateManagedSecretOutput(output) => { Ok(output.managed_secret) } CreateManagedSecretResult::UserFacingError(error) => { Err(anyhow!(get_user_facing_error_message(error))) } CreateManagedSecretResult::Unknown => { Err(anyhow!("Unknown error while creating managed secret")) } } } async fn delete_managed_secret( &self, request_scope: &Self::RequestScope, owner: SecretOwner, name: String, ) -> Result<()> { let graphql_owner = match owner { SecretOwner::CurrentUser => Owner { type_: OwnerType::User, uid: None, }, SecretOwner::Team { team_uid } => Owner { type_: OwnerType::Team, uid: Some(cynic::Id::new(team_uid)), }, }; let variables = DeleteManagedSecretVariables { input: DeleteManagedSecretInput { name, owner: graphql_owner, }, request_context: get_request_context(), }; let operation = DeleteManagedSecret::build(variables); let response = self .send_graphql_request_for_team(operation, *request_scope) .await?; match response.delete_managed_secret { DeleteManagedSecretResult::DeleteManagedSecretOutput(_) => Ok(()), DeleteManagedSecretResult::UserFacingError(error) => { Err(anyhow!(get_user_facing_error_message(error))) } DeleteManagedSecretResult::Unknown => { Err(anyhow!("Unknown error while deleting managed secret")) } } } async fn update_managed_secret( &self, request_scope: &Self::RequestScope, owner: SecretOwner, name: String, encrypted_value: Option, description: Option, ) -> Result { let graphql_owner = match owner { SecretOwner::CurrentUser => Owner { type_: OwnerType::User, uid: None, }, SecretOwner::Team { team_uid } => Owner { type_: OwnerType::Team, uid: Some(cynic::Id::new(team_uid)), }, }; let variables = UpdateManagedSecretVariables { input: UpdateManagedSecretInput { name, owner: graphql_owner, encrypted_value, description, }, request_context: get_request_context(), }; let operation = UpdateManagedSecret::build(variables); let response = self .send_graphql_request_for_team(operation, *request_scope) .await?; match response.update_managed_secret { UpdateManagedSecretResult::UpdateManagedSecretOutput(output) => { Ok(output.managed_secret) } UpdateManagedSecretResult::UserFacingError(error) => { Err(anyhow!(get_user_facing_error_message(error))) } UpdateManagedSecretResult::Unknown => { Err(anyhow!("Unknown error while updating managed secret")) } } } async fn list_harness_auth_secrets( &self, request_scope: &Self::RequestScope, harness: warp_graphql::ai::AgentHarness, ) -> Result> { let Some(harness_input) = Option::< warp_graphql::queries::list_harness_auth_secrets::AgentHarnessInput, >::from(harness) else { return Ok(vec![]); }; let variables = ListHarnessAuthSecretsVariables { input: ListHarnessAuthSecretsInput { harness: harness_input, }, request_context: get_request_context(), }; let operation = ListHarnessAuthSecrets::build(variables); let response = self .send_graphql_request_for_team(operation, *request_scope) .await?; match response.harness_auth_secrets { warp_graphql::queries::list_harness_auth_secrets::HarnessAuthSecretsResult::HarnessAuthSecretsOutput(output) => { Ok(output.managed_secrets) } warp_graphql::queries::list_harness_auth_secrets::HarnessAuthSecretsResult::UserFacingError(error) => { Err(anyhow!(get_user_facing_error_message(error))) } warp_graphql::queries::list_harness_auth_secrets::HarnessAuthSecretsResult::Unknown => { Err(anyhow!("Unknown error while listing harness auth secrets")) } } } async fn list_secrets( &self, request_scope: Option<&Self::RequestScope>, ) -> Result> { let variables = ListManagedSecretsVariables { // Pagination over managed secrets is not yet supported. input: ManagedSecretsInput { cursor: None }, request_context: get_request_context(), }; let operation = ListManagedSecrets::build(variables); let response = match request_scope { Some(request_scope) => { self.send_graphql_request_for_team(operation, *request_scope) .await? } None => self.send_graphql_request(operation, None).await?, }; match response.managed_secrets { ManagedSecretsResult::ManagedSecretsOutput(output) => Ok(output.managed_secrets), ManagedSecretsResult::UserFacingError(error) => { Err(anyhow!(get_user_facing_error_message(error))) } ManagedSecretsResult::Unknown => { Err(anyhow!("Unknown error while listing managed secrets")) } } } async fn get_task_secrets( &self, task_id: String, workload_token: String, ) -> Result> { let variables = TaskSecretsVariables { input: TaskSecretsInput { task_id: cynic::Id::new(task_id), workload_token, }, request_context: get_request_context(), }; let operation = TaskSecrets::build(variables); let response = self.send_graphql_request(operation, None).await?; match response.task_secrets { TaskSecretsResult::TaskSecretsOutput(output) => { let mut secrets = HashMap::new(); for entry in output.secrets { secrets.insert(entry.name, entry.value); } Ok(secrets) } TaskSecretsResult::UserFacingError(error) => { Err(anyhow!(get_user_facing_error_message(error))) } TaskSecretsResult::Unknown => Err(anyhow!("Unknown error while getting task secrets")), } } async fn issue_task_identity_token( &self, options: warp_managed_secrets::client::IdentityTokenOptions, ) -> Result { let requested_duration_seconds = options .requested_duration .as_secs() .try_into() .context("Requested duration out of bounds")?; let variables = IssueTaskIdentityTokenVariables { input: IssueTaskIdentityTokenInput { audience: options.audience, requested_duration_seconds, subject_template: Some(options.subject_template.into_vec()), }, request_context: get_request_context(), }; let operation = IssueTaskIdentityToken::build(variables); let response = self.send_graphql_request(operation, None).await?; match response.issue_task_identity_token { IssueTaskIdentityTokenResult::IssueTaskIdentityTokenOutput(output) => { Ok(TaskIdentityToken { token: output.token, expires_at: output.expires_at.utc(), issuer: output.issuer, }) } IssueTaskIdentityTokenResult::UserFacingError(error) => { Err(anyhow!(get_user_facing_error_message(error))) } IssueTaskIdentityTokenResult::Unknown => { Err(anyhow!("Unknown error while issuing task identity token")) } } } } #[cfg(test)] #[path = "managed_secrets_tests.rs"] mod tests;