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