wasmcloud_host/
policy.rs

1use std::collections::{BTreeMap, HashMap};
2use std::hash::Hash;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7use wascap::jwt;
8
9// NOTE: All requests will be v1 until the schema changes, at which point we can change the version
10// per-request type
11pub(crate) const POLICY_TYPE_VERSION: &str = "v1";
12
13/// A trait for evaluating policy decisions
14#[async_trait::async_trait]
15pub trait PolicyManager: Send + Sync {
16    /// Evaluate whether a component may be started
17    async fn evaluate_start_component(
18        &self,
19        _component_id: &str,
20        _image_ref: &str,
21        _max_instances: u32,
22        _annotations: &BTreeMap<String, String>,
23        _claims: Option<&jwt::Claims<jwt::Component>>,
24    ) -> anyhow::Result<Response> {
25        Ok(Response {
26            request_id: Uuid::new_v4().to_string(),
27            permitted: true,
28            message: None,
29        })
30    }
31
32    /// Evaluate whether a provider may be started
33    async fn evaluate_start_provider(
34        &self,
35        _provider_id: &str,
36        _provider_ref: &str,
37        _annotations: &BTreeMap<String, String>,
38        _claims: Option<&jwt::Claims<jwt::CapabilityProvider>>,
39    ) -> anyhow::Result<Response> {
40        Ok(Response {
41            request_id: Uuid::new_v4().to_string(),
42            permitted: true,
43            message: None,
44        })
45    }
46
47    /// Evaluate whether a component may perform an invocation
48    async fn evaluate_perform_invocation(
49        &self,
50        _component_id: &str,
51        _image_ref: &str,
52        _annotations: &BTreeMap<String, String>,
53        _claims: Option<&jwt::Claims<jwt::Component>>,
54        _interface: String,
55        _function: String,
56    ) -> anyhow::Result<Response> {
57        Ok(Response {
58            request_id: Uuid::new_v4().to_string(),
59            permitted: true,
60            message: None,
61        })
62    }
63}
64
65/// A default policy manager that always returns true for all requests
66/// This is used when no policy manager is configured
67#[derive(Default)]
68pub struct DefaultPolicyManager;
69impl super::PolicyManager for DefaultPolicyManager {}
70
71#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Hash)]
72/// Claims associated with a policy request, if embedded inside the component or provider
73pub struct PolicyClaims {
74    /// The public key of the component
75    #[serde(rename = "publicKey")]
76    pub public_key: String,
77    /// The issuer key of the component
78    pub issuer: String,
79    /// The time the claims were signed
80    #[serde(rename = "issuedAt")]
81    pub issued_at: String,
82    /// The time the claims expire, if any
83    #[serde(rename = "expiresAt")]
84    pub expires_at: Option<u64>,
85    /// Whether the claims have expired already. This is included in case the policy server is fulfilled by an component, which cannot access the system clock
86    pub expired: bool,
87}
88
89#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Hash)]
90/// Relevant policy information for evaluating a component
91pub struct ComponentInformation {
92    /// The unique identifier of the component
93    #[serde(rename = "componentId")]
94    pub component_id: String,
95    /// The image reference of the component
96    #[serde(rename = "imageRef")]
97    pub image_ref: String,
98    /// The requested maximum number of concurrent instances for this component
99    #[serde(rename = "maxInstances")]
100    pub max_instances: u32,
101    /// Annotations associated with the component
102    pub annotations: BTreeMap<String, String>,
103    /// Claims, if embedded, within the component
104    pub claims: Option<PolicyClaims>,
105}
106
107#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Hash)]
108/// Relevant policy information for evaluating a provider
109pub struct ProviderInformation {
110    /// The unique identifier of the provider
111    #[serde(rename = "providerId")]
112    pub provider_id: String,
113    /// The image reference of the provider
114    #[serde(rename = "imageRef")]
115    pub image_ref: String,
116    /// Annotations associated with the provider
117    pub annotations: BTreeMap<String, String>,
118    /// Claims, if embedded, within the provider
119    pub claims: Option<PolicyClaims>,
120}
121
122#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Hash)]
123/// A request to invoke a component function
124pub struct PerformInvocationRequest {
125    /// The interface of the invocation
126    pub interface: String,
127    /// The function of the invocation
128    pub function: String,
129    /// Target of the invocation
130    pub target: ComponentInformation,
131}
132
133/// Relevant information about the host that is receiving the invocation, or starting the component or provider
134#[derive(Clone, Debug, Serialize)]
135pub struct HostInfo {
136    /// The public key ID of the host
137    #[serde(rename = "publicKey")]
138    pub public_key: String,
139    /// The name of the lattice the host is running in
140    #[serde(rename = "lattice")]
141    pub lattice: String,
142    /// The labels associated with the host
143    pub labels: HashMap<String, String>,
144}
145
146/// The action being requested
147#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Hash)]
148pub enum RequestKind {
149    /// The host is checking whether it may invoke the target component
150    #[serde(rename = "performInvocation")]
151    PerformInvocation,
152    /// The host is checking whether it may start the target component
153    #[serde(rename = "startComponent")]
154    StartComponent,
155    /// The host is checking whether it may start the target provider
156    #[serde(rename = "startProvider")]
157    StartProvider,
158    /// An unknown or unsupported request type
159    #[serde(rename = "unknown")]
160    Unknown,
161}
162
163#[derive(Clone, Debug, Eq, PartialEq, Serialize, Hash)]
164#[serde(untagged)]
165/// The body of a policy request, typed by the request kind
166pub enum RequestBody {
167    /// A request to invoke a function on a component
168    PerformInvocation(PerformInvocationRequest),
169    /// A request to start a component on a host
170    StartComponent(ComponentInformation),
171    /// A request to start a provider on a host
172    StartProvider(ProviderInformation),
173    /// Request body has an unknown type
174    Unknown,
175}
176
177impl From<&RequestBody> for RequestKey {
178    fn from(val: &RequestBody) -> RequestKey {
179        match val {
180            RequestBody::StartComponent(ref req) => RequestKey {
181                kind: RequestKind::StartComponent,
182                cache_key: format!("{}_{}", req.component_id, req.image_ref),
183            },
184            RequestBody::StartProvider(ref req) => RequestKey {
185                kind: RequestKind::StartProvider,
186                cache_key: format!("{}_{}", req.provider_id, req.image_ref),
187            },
188            RequestBody::PerformInvocation(ref req) => RequestKey {
189                kind: RequestKind::PerformInvocation,
190                cache_key: format!(
191                    "{}_{}_{}_{}",
192                    req.target.component_id, req.target.image_ref, req.interface, req.function
193                ),
194            },
195            RequestBody::Unknown => RequestKey {
196                kind: RequestKind::Unknown,
197                cache_key: String::new(),
198            },
199        }
200    }
201}
202
203/// A request for a policy decision
204#[derive(Serialize)]
205pub(crate) struct Request {
206    /// A unique request id. This value is returned in the response
207    #[serde(rename = "requestId")]
208    #[allow(clippy::struct_field_names)]
209    pub(crate) request_id: String,
210    /// The kind of policy request being made
211    pub(crate) kind: RequestKind,
212    /// The version of the policy request body
213    pub(crate) version: String,
214    /// The policy request body
215    pub(crate) request: RequestBody,
216    /// Information about the host making the request
217    pub(crate) host: HostInfo,
218}
219
220#[derive(Clone, Debug, Hash, Eq, PartialEq)]
221pub(crate) struct RequestKey {
222    /// The kind of request being made
223    kind: RequestKind,
224    /// Information about this request combined to form a unique string.
225    /// For example, a StartComponent request can be uniquely cached based
226    /// on the component_id and image_ref, so this cache_key is a concatenation
227    /// of those values
228    cache_key: String,
229}
230
231/// A policy decision response
232#[derive(Clone, Debug, Deserialize)]
233pub struct Response {
234    /// The request id copied from the request
235    #[serde(rename = "requestId")]
236    pub request_id: String,
237    /// Whether the request is permitted
238    pub permitted: bool,
239    /// An optional error explaining why the request was denied. Suitable for logging
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub message: Option<String>,
242}
243
244fn is_expired(expires: u64) -> bool {
245    SystemTime::now()
246        .duration_since(UNIX_EPOCH)
247        .expect("time went backwards") // SAFETY: now() should always be greater than UNIX_EPOCH
248        .as_secs()
249        > expires
250}
251
252impl From<&jwt::Claims<jwt::Component>> for PolicyClaims {
253    fn from(claims: &jwt::Claims<jwt::Component>) -> Self {
254        PolicyClaims {
255            public_key: claims.subject.to_string(),
256            issuer: claims.issuer.to_string(),
257            issued_at: claims.issued_at.to_string(),
258            expires_at: claims.expires,
259            expired: claims.expires.is_some_and(is_expired),
260        }
261    }
262}
263
264impl From<&jwt::Claims<jwt::CapabilityProvider>> for PolicyClaims {
265    fn from(claims: &jwt::Claims<jwt::CapabilityProvider>) -> Self {
266        PolicyClaims {
267            public_key: claims.subject.to_string(),
268            issuer: claims.issuer.to_string(),
269            issued_at: claims.issued_at.to_string(),
270            expires_at: claims.expires,
271            expired: claims.expires.is_some_and(is_expired),
272        }
273    }
274}