Skip to main content

gaia_client/
client.rs

1use crate::config::GaiaClientConfig;
2use crate::error::Result;
3use crate::proto::gaia_client_client::GaiaClientClient;
4use crate::proto::{
5    GetSecretRequest, Namespace, Secret,
6};
7use crate::tls::create_tls_channel;
8use tonic::transport::Channel;
9
10/// Options for loading secrets into the environment.
11#[derive(Debug, Clone, Default)]
12pub struct LoadEnvOptions {
13    /// Prefix to prepend to environment variable names.
14    /// If strictly empty or None, no prefix is added.
15    pub prefix: Option<String>,
16    
17    /// Whether to include the namespace in the environment variable name.
18    pub use_namespace: bool,
19}
20
21impl LoadEnvOptions {
22    /// Create new options with default values (key only).
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Set the prefix.
28    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
29        self.prefix = Some(prefix.into());
30        self
31    }
32
33    /// Set whether to use the namespace.
34    pub fn with_namespace(mut self, use_namespace: bool) -> Self {
35        self.use_namespace = use_namespace;
36        self
37    }
38}
39
40/// A client for interacting with the Gaia secret management daemon.
41///
42/// The client uses mutual TLS (mTLS) for secure communication and provides
43/// methods to retrieve secrets and check daemon status.
44pub struct GaiaClient {
45    inner: GaiaClientClient<Channel>,
46}
47
48impl GaiaClient {
49    /// Connects to the Gaia daemon using the provided configuration.
50    ///
51    /// # Arguments
52    ///
53    /// * `config` - Configuration containing server address and TLS certificates
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if:
58    /// - TLS certificates cannot be loaded
59    /// - Connection to the daemon fails
60    ///
61    /// # Example
62    ///
63    /// ```no_run
64    /// use gaia_client::{GaiaClient, GaiaClientConfig};
65    ///
66    /// #[tokio::main]
67    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
68    ///     let config = GaiaClientConfig::new(
69    ///         "localhost:50051",
70    ///         "/etc/gaia/certs/ca.crt",
71    ///         "/etc/gaia/certs/client.crt",
72    ///         "/etc/gaia/certs/client.key",
73    ///     );
74    ///
75    ///     let client = GaiaClient::connect(config).await?;
76    ///     Ok(())
77    /// }
78    /// ```
79    pub async fn connect(config: GaiaClientConfig) -> Result<Self> {
80        let channel = create_tls_channel(&config).await?;
81        let inner = GaiaClientClient::new(channel);
82        Ok(Self { inner })
83    }
84
85    /// Retrieves a secret from the specified namespace.
86    ///
87    /// # Arguments
88    ///
89    /// * `namespace` - The namespace containing the secret
90    /// * `id` - The secret identifier
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if:
95    /// - The daemon is locked
96    /// - The secret does not exist
97    /// - A network error occurs
98    ///
99    /// # Example
100    ///
101    /// ```no_run
102    /// # use gaia_client::{GaiaClient, GaiaClientConfig};
103    /// # #[tokio::main]
104    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
105    /// # let config = GaiaClientConfig::new("localhost:50051", "ca.crt", "client.crt", "client.key");
106    /// # let mut client = GaiaClient::connect(config).await?;
107    /// let secret = client.get_secret("production", "database_url").await?;
108    /// println!("Secret: {}", secret.value);
109    /// # Ok(())
110    /// # }
111    /// ```
112    pub async fn get_secret(&mut self, namespace: &str, id: &str) -> Result<Secret> {
113        let request = tonic::Request::new(GetSecretRequest {
114            namespace: namespace.to_string(),
115            id: id.to_string(),
116        });
117
118        let response = self.inner.get_secret(request).await?;
119        Ok(response.into_inner())
120    }
121
122
123
124
125
126
127
128
129
130    /// Lists all secrets for the authenticated client.
131    ///
132    /// Returns secrets from the client's own namespaces plus the common namespace.
133    /// If a namespace filter is provided, only secrets from that namespace are returned.
134    ///
135    /// # Arguments
136    ///
137    /// * `namespace` - Optional namespace filter
138    ///
139    /// # Example
140    ///
141    /// ```no_run
142    /// # use gaia_client::{GaiaClient, GaiaClientConfig};
143    /// # #[tokio::main]
144    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
145    /// # let config = GaiaClientConfig::new("localhost:50051", "ca.crt", "client.crt", "client.key");
146    /// # let mut client = GaiaClient::connect(config).await?;
147    /// // Get all secrets (client's own + common)
148    /// let all_secrets = client.list_secrets(None).await?;
149    ///
150    /// // Get only secrets from a specific namespace
151    /// let prod_secrets = client.list_secrets(Some("production".to_string())).await?;
152    ///
153    /// for namespace in all_secrets {
154    ///     println!("Namespace: {}", namespace.name);
155    ///     for secret in namespace.secrets {
156    ///         println!("  - {}: {}", secret.id, secret.value);
157    ///     }
158    /// }
159    /// # Ok(())
160    /// # }
161    /// ```
162    pub async fn list_secrets(&mut self, namespace: Option<String>) -> Result<Vec<Namespace>> {
163        use crate::proto::ClientListSecretsRequest;
164
165        let request = tonic::Request::new(ClientListSecretsRequest {
166            namespace: namespace.unwrap_or_default(),
167        });
168
169        let response = self.inner.list_secrets(request).await?;
170        Ok(response.into_inner().namespaces)
171    }
172
173    /// Fetches all accessible secrets and loads them into the current process's environment.
174    ///
175    /// By default, environment variables are named after the secret key, converted to uppercase
176    /// with hyphens replaced by underscores. Optional prefix and namespace inclusion can be
177    /// configured via `LoadEnvOptions`.
178    ///
179    /// # Arguments
180    ///
181    /// * `options` - Optional configuration for env var naming
182    ///
183    /// # Example
184    ///
185    /// ```no_run
186    /// # use gaia_client::{GaiaClient, GaiaClientConfig, LoadEnvOptions};
187    /// # #[tokio::main]
188    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
189    /// # let config = GaiaClientConfig::new("localhost:50051", "ca.crt", "client.crt", "client.key");
190    /// # let mut client = GaiaClient::connect(config).await?;
191    /// // Load with default behavior (key only)
192    /// client.load_env(None).await?;
193    ///
194    /// // Load with prefix and namespace: GAIA_PRODUCTION_DATABASE_URL
195    /// client.load_env(Some(LoadEnvOptions::new().with_prefix("GAIA").with_namespace(true))).await?;
196    /// # Ok(())
197    /// # }
198    /// ```
199    pub async fn load_env(&mut self, options: Option<LoadEnvOptions>) -> Result<()> {
200        let opts = options.unwrap_or_default();
201        let namespaces = self.list_secrets(None).await?;
202
203        for ns in namespaces {
204            for secret in ns.secrets {
205                let mut parts = Vec::new();
206
207                if let Some(prefix) = &opts.prefix {
208                    if !prefix.is_empty() {
209                        parts.push(prefix.to_uppercase().replace("-", "_"));
210                    }
211                }
212
213                if opts.use_namespace {
214                    parts.push(ns.name.to_uppercase().replace("-", "_"));
215                }
216
217                parts.push(secret.id.to_uppercase().replace("-", "_"));
218
219                let env_var_name = parts.join("_");
220                std::env::set_var(env_var_name, secret.value);
221            }
222        }
223
224        Ok(())
225    }
226}