wasmcloud_secrets_types/
lib.rs

1use anyhow::{ensure, Context as _};
2use async_trait::async_trait;
3use nkeys::XKey;
4use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
5use std::collections::HashMap;
6use wascap::jwt::{validate_token, CapabilityProvider, Component, Host};
7
8mod errors;
9pub use crate::errors::*;
10
11/// The version of the secrets API
12pub const SECRET_API_VERSION: &str = "v1alpha1";
13
14/// The key of a NATS header containing the wasmCloud host's public xkey used to encrypt a secret request.
15/// It is also used to encrypt the response so that only the requestor can decrypt it.
16pub const WASMCLOUD_HOST_XKEY: &str = "WasmCloud-Host-Xkey";
17pub const RESPONSE_XKEY: &str = "Server-Response-Xkey";
18
19/// The type of secret.
20/// This is used to inform wadm or anything else that is consuming the secret about how to
21/// deserialize the payload.
22pub const SECRET_TYPE: &str = "secret.wasmcloud.dev/v1alpha1";
23
24/// The type of the properties in the secret policy.
25/// This is primarily used to version the policy properties format.
26pub const SECRET_POLICY_PROPERTIES_TYPE: &str = "properties.secret.wasmcloud.dev/v1alpha1";
27
28/// The prefix for all secret keys in the config store
29pub const SECRET_PREFIX: &str = "SECRET";
30
31/// The request context for retrieving a secret
32#[derive(Serialize, Deserialize, Default)]
33pub struct Context {
34    /// The component or provider's signed JWT.
35    pub entity_jwt: String,
36    /// The host's signed JWT.
37    pub host_jwt: String,
38    /// Information about the application that the entity belongs to.
39    pub application: Application,
40}
41
42/// The application that the entity belongs to.
43#[derive(Serialize, Deserialize, Default)]
44pub struct Application {
45    /// The name of the application.
46    #[serde(default)]
47    pub name: Option<String>,
48
49    /// The policy used define the application's access to secrets.
50    /// This meant to be a JSON string that can be deserialized by a secrets backend
51    /// implementation.
52    #[serde(default)]
53    pub policy: String,
54}
55
56impl Context {
57    /// Validates that the underlying claims embedded in the Context's JWTs are valid.
58    pub fn valid_claims(&self) -> Result<(), ContextValidationError> {
59        let component_valid = Self::valid_component(&self.entity_jwt);
60        let provider_valid = Self::valid_provider(&self.entity_jwt);
61        // TODO: There's almost certainly a bug here, it was ported from original conditional commented-out below:
62        //if component_valid.is_err() && provider_valid.is_err() {
63        //    if let Err(e) = component_valid {
64        //        return Err(ContextValidationError::InvalidComponentJWT(e.to_string()));
65        //    } else {
66        //        return Err(ContextValidationError::InvalidProviderJWT(
67        //            provider_valid.unwrap_err().to_string(),
68        //        ));
69        //    }
70        //}
71        if provider_valid.is_err() {
72            if let Err(e) = component_valid {
73                return Err(ContextValidationError::InvalidComponentJWT(e.to_string()));
74            }
75        }
76
77        if Self::valid_host(&self.host_jwt).is_err() {
78            return Err(ContextValidationError::InvalidHostJWT(
79                Self::valid_host(&self.host_jwt).unwrap_err().to_string(),
80            ));
81        }
82        Ok(())
83    }
84
85    fn valid_component(token: &str) -> anyhow::Result<()> {
86        let v = validate_token::<Component>(token)?;
87        ensure!(!v.expired, "token expired at `{}`", v.expires_human);
88        ensure!(
89            !v.cannot_use_yet,
90            "token cannot be used before `{}`",
91            v.not_before_human
92        );
93        ensure!(v.signature_valid, "signature is not valid");
94        Ok(())
95    }
96
97    fn valid_provider(token: &str) -> anyhow::Result<()> {
98        let v = validate_token::<CapabilityProvider>(token)?;
99        ensure!(!v.expired, "token expired at `{}`", v.expires_human);
100        ensure!(
101            !v.cannot_use_yet,
102            "token cannot be used before `{}`",
103            v.not_before_human
104        );
105        ensure!(v.signature_valid, "signature is not valid");
106
107        Ok(())
108    }
109
110    fn valid_host(token: &str) -> anyhow::Result<()> {
111        let v = validate_token::<Host>(token)?;
112        ensure!(!v.expired, "token expired at `{}`", v.expires_human);
113        ensure!(
114            !v.cannot_use_yet,
115            "token cannot be used before `{}`",
116            v.not_before_human
117        );
118        ensure!(v.signature_valid, "signature is not valid");
119        Ok(())
120    }
121}
122
123/// The request to retrieve a secret. This includes the name of the secret and the context needed
124/// to validate the requestor. The context will be passed to the underlying secrets service in
125/// order to make decisions around access.
126/// The version field is optional but highly recommended. If it is not provided, the service will
127/// default to retrieving the latest version of the secret.
128#[derive(Serialize, Deserialize)]
129pub struct SecretRequest {
130    /// An identifier of the secret as stored in the secret store.
131    ///
132    /// This can be a key, path, or any other identifier that the secret store uses to
133    /// retrieve a secret.
134    pub key: String,
135    pub field: Option<String>,
136    // The version of the secret
137    pub version: Option<String>,
138    pub context: Context,
139}
140
141/// The response to a secret request. The fields are mutually exclusive: either a secret or an
142/// error will be set.
143#[derive(Serialize, Deserialize, Default)]
144pub struct SecretResponse {
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub secret: Option<Secret>,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub error: Option<GetSecretError>,
149}
150
151/// A secret that can be either a string or binary value.
152#[derive(Serialize, Deserialize, Default)]
153pub struct Secret {
154    pub version: String,
155    pub string_secret: Option<String>,
156    pub binary_secret: Option<Vec<u8>>,
157}
158
159/// The representation of a secret reference in the config store.
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct SecretConfig {
162    /// The name of the secret when referred to by a component or provider.
163    pub name: String,
164    /// The backend to use for retrieving the secret.
165    pub backend: String,
166    /// The key to use for retrieving the secret from the backend.
167    pub key: String,
168    /// The field to retrieve from the secret. If not supplied, the entire secret will be returned.
169    pub field: Option<String>,
170    /// The version of the secret to retrieve. If not supplied, the latest version will be used.
171    pub version: Option<String>,
172    /// The policy that defines configuration options for the backend. This is a serialized
173    /// JSON object that will be passed to the backend as a string for policy evaluation.
174    pub policy: Policy,
175
176    // NOTE: Should be serialized/deserialized as "type" in JSON
177    /// The type of secret.
178    /// This is used to inform wadm or anything else that is consuming the secret about how to
179    /// deserialize the payload.
180    pub secret_type: String,
181}
182
183impl SecretConfig {
184    pub fn new(
185        name: String,
186        backend: String,
187        key: String,
188        field: Option<String>,
189        version: Option<String>,
190        policy_properties: HashMap<String, serde_json::Value>,
191    ) -> Self {
192        Self {
193            name,
194            backend,
195            key,
196            field,
197            version,
198            policy: Policy::new(policy_properties),
199            secret_type: SECRET_TYPE.to_string(),
200        }
201    }
202
203    /// Given an entity JWT, host JWT, and optional application name, convert this SecretConfig
204    /// into a SecretRequest that can be used to fetch the secret from a secrets backend.
205    ///
206    /// This is not a true [`TryInto`] implementation as we need additional information to create
207    /// the [`SecretRequest`]. This returns an error if the policy field cannot be serialized to a JSON
208    /// string.
209    pub fn try_into_request(
210        self,
211        entity_jwt: &str,
212        host_jwt: &str,
213        application_name: Option<&String>,
214    ) -> Result<SecretRequest, anyhow::Error> {
215        Ok(SecretRequest {
216            key: self.key,
217            field: self.field,
218            version: self.version,
219            context: Context {
220                entity_jwt: entity_jwt.to_string(),
221                host_jwt: host_jwt.to_string(),
222                application: Application {
223                    name: application_name.cloned(),
224                    policy: serde_json::to_string(&self.policy)
225                        .context("failed to serialize secret policy as string")?,
226                },
227            },
228        })
229    }
230}
231
232/// Helper function to convert a SecretConfig into a HashMap. This is only intended to be used by
233/// wash or anything else that needs to interact directly with the config KV bucket to manipulate
234/// secrets.
235impl TryInto<HashMap<String, String>> for SecretConfig {
236    type Error = anyhow::Error;
237
238    /// Convert this SecretConfig into a HashMap of the form:
239    /// ```json
240    /// {
241    ///   "name": "secret-name",
242    ///   "type": "secret.wasmcloud.dev/v1alpha1",
243    ///   "backend": "baobun",
244    ///   "key": "/path/to/secret",
245    ///   "version": "vX.Y.Z",
246    ///   "policy": "{\"type\":\"properties.secret.wasmcloud.dev/v1alpha1\",\"properties\":{\"key\":\"value\"}}"
247    /// }
248    /// ```
249    fn try_into(self) -> Result<HashMap<String, String>, Self::Error> {
250        let mut map = HashMap::from([
251            ("name".into(), self.name),
252            ("type".into(), self.secret_type),
253            ("backend".into(), self.backend),
254            ("key".into(), self.key),
255        ]);
256        if let Some(field) = self.field {
257            map.insert("field".to_string(), field);
258        }
259        if let Some(version) = self.version {
260            map.insert("version".to_string(), version);
261        }
262
263        map.insert(
264            "policy".to_string(),
265            serde_json::to_string(&self.policy).context("failed to serialize policy string")?,
266        );
267        Ok(map)
268    }
269}
270
271// We need full impls of serialize and deserialize because we have to handle the custom error case
272// when serializing the policy to JSON
273impl Serialize for SecretConfig {
274    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
275    where
276        S: Serializer,
277    {
278        let field_count = if self.version.is_some() { 6 } else { 5 };
279        let mut state = serializer.serialize_struct("SecretReference", field_count)?;
280        state.serialize_field("name", &self.name)?;
281        state.serialize_field("backend", &self.backend)?;
282        state.serialize_field("key", &self.key)?;
283        if let Some(v) = self.version.as_ref() {
284            state.serialize_field("version", v)?;
285        }
286
287        // Serialize policy to JSON string
288        let policy_json = serde_json::to_string(&self.policy).map_err(serde::ser::Error::custom)?;
289        state.serialize_field("policy", &policy_json)?;
290        state.serialize_field("type", &self.secret_type)?;
291
292        state.end()
293    }
294}
295
296impl<'de> Deserialize<'de> for SecretConfig {
297    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
298    where
299        D: Deserializer<'de>,
300    {
301        #[derive(Deserialize)]
302        struct Helper {
303            name: String,
304            backend: String,
305            key: String,
306            field: Option<String>,
307            version: Option<String>,
308            policy: String,
309            #[serde(rename = "type")]
310            ty: String,
311        }
312
313        let helper = Helper::deserialize(deserializer)?;
314
315        // Deserialize policy from JSON string
316        let policy: Policy =
317            serde_json::from_str(&helper.policy).map_err(serde::de::Error::custom)?;
318
319        Ok(SecretConfig {
320            name: helper.name,
321            backend: helper.backend,
322            key: helper.key,
323            field: helper.field,
324            version: helper.version,
325            policy,
326            secret_type: helper.ty,
327        })
328    }
329}
330
331#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
332pub struct Policy {
333    #[serde(rename = "type")]
334    policy_type: String,
335    properties: HashMap<String, serde_json::Value>,
336}
337
338impl Default for Policy {
339    fn default() -> Self {
340        Self {
341            policy_type: SECRET_POLICY_PROPERTIES_TYPE.to_string(),
342            properties: Default::default(),
343        }
344    }
345}
346
347impl Policy {
348    /// Returns a new policy with the specified properties
349    pub fn new(properties: HashMap<String, serde_json::Value>) -> Self {
350        Self {
351            properties,
352            ..Default::default()
353        }
354    }
355}
356
357#[async_trait]
358pub trait SecretsServer {
359    // Returns the secret value for the given secret name
360    async fn get(&self, request: SecretRequest) -> Result<SecretResponse, GetSecretError>;
361
362    // Returns the server's public XKey
363    fn server_xkey(&self) -> XKey;
364}
365
366#[cfg(test)]
367mod test {
368    use std::collections::HashMap;
369    #[test]
370    fn test_secret_config_hashmap_try_into() {
371        let properties = HashMap::from([(
372            String::from("key"),
373            serde_json::Value::String("value".to_string()),
374        )]);
375        let secret_config = crate::SecretConfig::new(
376            "name".to_string(),
377            "backend".to_string(),
378            "key".to_string(),
379            Some("field".to_string()),
380            Some("version".to_string()),
381            properties,
382        );
383
384        let map: HashMap<String, String> = secret_config
385            .clone()
386            .try_into()
387            .expect("should be able to convert to hashmap");
388
389        assert_eq!(map.get("name"), Some(&secret_config.name));
390        assert_eq!(map.get("type"), Some(&secret_config.secret_type));
391        assert_eq!(map.get("backend"), Some(&secret_config.backend));
392        assert_eq!(map.get("key"), Some(&secret_config.key));
393        assert_eq!(map.get("field"), secret_config.field.as_ref());
394        assert_eq!(map.get("version"), secret_config.version.as_ref());
395        assert_eq!(
396            map.get("policy"),
397            Some(
398                &serde_json::to_string(&secret_config.policy)
399                    .expect("should be able to serialize policy")
400            )
401        );
402    }
403}