oso_cloud/
lib.rs

1use polar_ast::{PRIMITIVE_BOOLEAN_SYMBOL, PRIMITIVE_INTEGER_SYMBOL, PRIMITIVE_STRING_SYMBOL};
2use serde::{Deserialize, Serialize};
3use std::{
4    borrow::Cow,
5    collections::{BTreeMap, HashMap},
6    fmt::Debug,
7};
8
9mod api;
10mod polar_ast;
11
12#[cfg(feature = "local-filtering")]
13mod local_filtering;
14#[cfg(feature = "local-filtering")]
15pub use local_filtering::*;
16
17#[macro_use]
18mod macros;
19
20#[derive(thiserror::Error, Debug)]
21pub enum Error {
22    #[error("Api Error: {0}")]
23    Api(#[from] reqwest::Error),
24    #[error("Oso Server Error: {message}")]
25    Server {
26        message: String,
27        request_id: Option<String>,
28    },
29    #[error("Input error: {0}")]
30    Input(String),
31}
32
33impl Error {
34    pub fn api_request_id(&self) -> Option<&str> {
35        match self {
36            Error::Server { request_id, .. } => request_id.as_deref(),
37            _ => None,
38        }
39    }
40}
41
42#[derive(Clone)]
43pub struct Oso {
44    environment_id: String,
45    client: api::Client,
46}
47
48impl Debug for Oso {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("Oso")
51            .field("url", &self.client.url)
52            .field("environment_id", &self.environment_id)
53            .finish()
54    }
55}
56
57type StringRef<'a> = Cow<'a, str>;
58
59/// Representation of values used in Oso Cloud
60///
61/// All values are represented as a `type` and an `id`, both of which are strings
62#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
63pub struct Value<'a> {
64    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
65    pub type_: Option<StringRef<'a>>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub id: Option<StringRef<'a>>,
68}
69
70impl<'a> Value<'a> {
71    /// Construct a new Value
72    pub fn new(type_: impl Into<StringRef<'a>>, id: impl Into<StringRef<'a>>) -> Self {
73        Self {
74            type_: Some(type_.into()),
75            id: Some(id.into()),
76        }
77    }
78
79    /// Construct a variable Value that can be bound to any value
80    pub fn any() -> Self {
81        Self { type_: None, id: None }
82    }
83
84    /// Construct a variable Value that can be bound to any value of a given type
85    pub fn any_of_type(type_: impl Into<StringRef<'a>>) -> Self {
86        Self {
87            type_: Some(type_.into()),
88            id: None,
89        }
90    }
91}
92
93impl From<bool> for Value<'static> {
94    fn from(b: bool) -> Self {
95        Self::new(PRIMITIVE_BOOLEAN_SYMBOL, b.to_string())
96    }
97}
98
99impl From<i64> for Value<'static> {
100    fn from(i: i64) -> Self {
101        Self::new(PRIMITIVE_INTEGER_SYMBOL, i.to_string())
102    }
103}
104
105impl<'a> From<&'a str> for Value<'a> {
106    fn from(s: &'a str) -> Self {
107        Self::new(PRIMITIVE_STRING_SYMBOL, s)
108    }
109}
110
111impl From<String> for Value<'static> {
112    fn from(s: String) -> Self {
113        Self::new(PRIMITIVE_STRING_SYMBOL, s)
114    }
115}
116
117#[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord, Clone)]
118pub struct Fact<'a> {
119    pub predicate: String,
120    pub args: Vec<Value<'a>>,
121}
122
123#[derive(Debug, Default, Serialize, Deserialize)]
124pub struct ResourceMetadata {
125    roles: Vec<String>,
126    permissions: Vec<String>,
127    relations: BTreeMap<String, String>,
128}
129
130#[derive(Debug, Default, Serialize, Deserialize)]
131pub struct PolicyMetadata {
132    pub resources: BTreeMap<String, ResourceMetadata>,
133}
134
135pub struct Builder {
136    url: String,
137    api_key: String,
138}
139
140impl Default for Builder {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl Builder {
147    pub fn new() -> Self {
148        Self {
149            url: "https://api.osohq.com".to_owned(),
150            api_key: "".to_owned(),
151        }
152    }
153
154    pub fn with_url(&mut self, url: &str) -> &mut Self {
155        url.clone_into(&mut self.url);
156        self
157    }
158
159    pub fn with_api_key(&mut self, api_key: &str) -> &mut Self {
160        api_key.clone_into(&mut self.api_key);
161        self
162    }
163
164    /// Create a new Oso client from environment variables.
165    ///
166    /// The following environment variables are used:
167    ///
168    /// * `OSO_URL` - The URL of the Oso Cloud instance to connect to. Defaults to `https://api.osohq.com`.
169    /// * `OSO_AUTH` - The API key to use when connecting to Oso Cloud. Defaults to an empty string.
170    pub fn from_env() -> Self {
171        let mut builder = Builder::new();
172        if let Ok(url) = std::env::var("OSO_URL") {
173            builder.with_url(&url);
174        }
175        if let Ok(api_key) = std::env::var("OSO_AUTH") {
176            builder.with_api_key(&api_key);
177        }
178        builder
179    }
180
181    pub fn build(&self) -> Result<Oso, Error> {
182        if self.api_key.is_empty() {
183            return Err(Error::Input("API key must be set".to_owned()));
184        }
185
186        Oso::new_with_url(&self.url, &self.api_key)
187    }
188}
189
190// Generic result from the API, for things that don't need to return anything besides a message.
191#[derive(Deserialize)]
192struct ApiResult {
193    pub message: String,
194}
195
196pub struct OsoWithContext<'a> {
197    client: &'a api::Client,
198    context: Vec<Fact<'a>>,
199}
200
201impl Oso {
202    pub fn new_with_url(url: &str, api_key: &str) -> Result<Self, Error> {
203        let client = api::Client::new(url, api_key)?;
204        let environment_id = api_key.split('_').take(2).collect::<Vec<_>>().join("_");
205        Ok(Self { client, environment_id })
206    }
207
208    pub fn new(api_key: &str) -> Result<Self, Error> {
209        Oso::new_with_url("https://api.osohq.com", api_key)
210    }
211
212    /// Update the current policy for the environment
213    pub async fn policy(&self, policy_src: &str) -> Result<(), Error> {
214        #[derive(Debug, Serialize)]
215        struct PolicyRequest<'a> {
216            src: &'a str,
217        }
218
219        let body = PolicyRequest { src: policy_src };
220        let res: ApiResult = self.client.post("policy", &body, true).await?;
221        tracing::info!("Policy updated: {}", res.message);
222
223        Ok(())
224    }
225
226    /// Get metadata about the currently active policy.
227    pub async fn get_policy_metadata(&self) -> Result<PolicyMetadata, Error> {
228        #[derive(Debug, Deserialize)]
229        struct MetadataResponse {
230            metadata: PolicyMetadata,
231        }
232        let res: MetadataResponse = self.client.get("policy_metadata", ()).await?;
233        Ok(res.metadata)
234    }
235
236    /// Add a single fact into Oso Cloud
237    pub async fn tell(&self, fact: Fact<'_>) -> Result<(), Error> {
238        self.bulk(&[], &[fact]).await
239    }
240
241    /// Deleta a fact
242    pub async fn delete(&self, fact: Fact<'_>) -> Result<(), Error> {
243        self.bulk(&[fact], &[]).await
244    }
245
246    /// Add multiple facts into Oso Cloud
247    pub async fn bulk_tell(&self, facts: &[Fact<'_>]) -> Result<(), Error> {
248        self.bulk(&[], facts).await
249    }
250
251    /// Deletes many facts at once. Does not throw an error when some of the facts are not found.
252    ///
253    /// `Value::any` and `Value::any_of_type` can be used as a wildcard in facts in delete.
254    /// Does not throw an error when the facts to delete are not found.
255    pub async fn bulk_delete(&self, facts: &[Fact<'_>]) -> Result<(), Error> {
256        self.bulk(facts, &[]).await
257    }
258
259    /// Deletes and adds many facts in one atomic transaction. The deletions are performed before the adds.
260    /// `Value::any` and `Value::any_of_type` can be used as a wildcard in facts in delete.
261    /// Does not throw an error when the facts to delete are not found.
262    pub async fn bulk(&self, delete: &[Fact<'_>], tell: &[Fact<'_>]) -> Result<(), Error> {
263        self.client.bulk(delete, tell).await
264    }
265
266    /// Lists facts that are stored in Oso Cloud. Can be used to check the existence of a particular fact,
267    /// or used to fetch all facts that have a particular argument
268    pub async fn get(&self, fact: &Fact<'_>) -> Result<Vec<Fact<'static>>, Error> {
269        let mut params = HashMap::new();
270
271        for (i, a) in fact.args.iter().enumerate() {
272            params.insert(format!("args.{i}.type"), a.type_.to_owned());
273            params.insert(format!("args.{i}.id"), a.id.to_owned());
274        }
275
276        self.client.get("facts", params).await
277    }
278
279    fn with_no_context(&self) -> OsoWithContext<'_> {
280        OsoWithContext {
281            client: &self.client,
282            context: vec![],
283        }
284    }
285
286    pub async fn authorize(
287        &self,
288        actor: impl Into<Value<'_>>,
289        action: &str,
290        resource: impl Into<Value<'_>>,
291    ) -> Result<bool, Error> {
292        self.with_no_context().authorize(actor, action, resource).await
293    }
294
295    pub async fn authorize_resources<T>(
296        &self,
297        actor: impl Into<Value<'_>>,
298        action: &str,
299        resources: &mut Vec<T>,
300    ) -> Result<(), Error>
301    where
302        for<'r> &'r T: Into<Value<'r>>,
303    {
304        self.with_no_context()
305            .authorize_resources(actor, action, resources)
306            .await
307    }
308
309    pub async fn actions(
310        &self,
311        actor: impl Into<Value<'_>>,
312        resource: impl Into<Value<'_>>,
313    ) -> Result<Vec<String>, Error> {
314        self.with_no_context().actions(actor, resource).await
315    }
316
317    pub async fn list(
318        &self,
319        actor: impl Into<Value<'_>>,
320        action: &str,
321        resource_type: &str,
322    ) -> Result<Vec<String>, Error> {
323        self.with_no_context().list(actor, action, resource_type).await
324    }
325
326    /// Query Oso Cloud for any predicate and any combination of concrete and wildcard arguments.
327    ///
328    /// Unlike `oso.get`, which only lists facts you've added, you can use `oso.query` to list
329    /// derived information about any rule in your policy.
330    pub async fn query(&self, fact: &Fact<'_>) -> Result<Vec<Fact<'static>>, Error> {
331        self.with_no_context().query(fact).await
332    }
333
334    pub fn with_context<'a>(&'a self, context: Vec<Fact<'a>>) -> OsoWithContext<'a> {
335        OsoWithContext {
336            client: &self.client,
337            context,
338        }
339    }
340}
341
342impl OsoWithContext<'_> {
343    pub async fn authorize(
344        &self,
345        actor: impl Into<Value<'_>>,
346        action: &str,
347        resource: impl Into<Value<'_>>,
348    ) -> Result<bool, Error> {
349        #[derive(Debug, Serialize)]
350        struct AuthorizeRequest<'a> {
351            actor_type: &'a str,
352            actor_id: &'a str,
353            action: &'a str,
354            resource_type: &'a str,
355            resource_id: &'a str,
356            context_facts: &'a [Fact<'a>],
357        }
358
359        let actor = actor.into();
360        let resource = resource.into();
361        let (Some(actor_type), Some(actor_id), Some(resource_type), Some(resource_id)) = (
362            actor.type_.as_ref(),
363            actor.id.as_ref(),
364            resource.type_.as_ref(),
365            resource.id.as_ref(),
366        ) else {
367            if actor.type_.is_none() || actor.id.is_none() {
368                return Err(Error::Input(
369                    "Actor must be a concrete value. Try `oso.query` if you want to get all permitted actors"
370                        .to_owned(),
371                ));
372            }
373            if resource.type_.is_none() || resource.id.is_none() {
374                return Err(Error::Input(
375                    "Resource must be a concrete value. Try `oso.list` if you want to get all allowed resources"
376                        .to_owned(),
377                ));
378            }
379            unreachable!();
380        };
381
382        let body = AuthorizeRequest {
383            actor_type,
384            actor_id,
385            action,
386            resource_type,
387            resource_id,
388            context_facts: &self.context,
389        };
390
391        #[derive(Deserialize)]
392        struct AuthorizeResponse {
393            allowed: bool,
394        }
395
396        let resp: AuthorizeResponse = self.client.post("authorize", &body, false).await?;
397
398        Ok(resp.allowed)
399    }
400
401    pub async fn authorize_resources<T>(
402        &self,
403        actor: impl Into<Value<'_>>,
404        action: &str,
405        resources: &mut Vec<T>,
406    ) -> Result<(), Error>
407    where
408        for<'r> &'r T: Into<Value<'r>>,
409    {
410        #[derive(Debug, Serialize)]
411        struct AuthorizeResourcesRequest<'a> {
412            actor_type: &'a str,
413            actor_id: &'a str,
414            action: &'a str,
415            resources: &'a Vec<Value<'a>>,
416            context_facts: &'a Vec<Fact<'a>>,
417        }
418
419        let resource_values = resources.iter().map(|r| r.into()).collect();
420
421        #[derive(Deserialize)]
422        struct AuthorizeResourcesResponse<'a> {
423            results: Vec<Value<'a>>,
424        }
425
426        let actor = actor.into();
427        let (Some(actor_type), Some(actor_id)) = (actor.type_.as_ref(), actor.id.as_ref()) else {
428            return Err(Error::Input(
429                "Actor must be a concrete value. Try `oso.query` if you want to get all permitted actors".to_owned(),
430            ));
431        };
432
433        let body = AuthorizeResourcesRequest {
434            actor_type,
435            actor_id,
436            action,
437            resources: &resource_values,
438            context_facts: &self.context,
439        };
440
441        let resp: AuthorizeResourcesResponse = self.client.post("authorize_resources", &body, false).await?;
442
443        if resp.results.len() == resources.len() {
444            // All resources were authorized, no filtering needed
445            return Ok(());
446        }
447
448        let mut results_iter = resp.results.into_iter();
449        let mut next = results_iter.next();
450
451        // Filter resources in place
452        //
453        // Since Oso Cloud returns resources in the same order they were passed in, we can
454        // iterate over the resources and results in parallel and remove any resources that
455        // are missing from the authorized set
456        resources.retain(|val| {
457            if let Some(ref next_val) = next {
458                if next_val == &val.into() {
459                    next = results_iter.next();
460                    return true;
461                }
462            }
463            false
464        });
465
466        Ok(())
467    }
468
469    pub async fn actions(
470        &self,
471        actor: impl Into<Value<'_>>,
472        resource: impl Into<Value<'_>>,
473    ) -> Result<Vec<String>, Error> {
474        #[derive(Debug, Serialize)]
475        struct ActionsRequest<'a> {
476            actor_type: &'a str,
477            actor_id: &'a str,
478            resource_type: &'a str,
479            resource_id: &'a str,
480            context_facts: &'a Vec<Fact<'a>>,
481        }
482
483        #[derive(Deserialize)]
484        struct ActionsResponse {
485            results: Vec<String>,
486        }
487
488        let actor = actor.into();
489        let resource = resource.into();
490
491        let (Some(actor_type), Some(actor_id), Some(resource_type), Some(resource_id)) = (
492            actor.type_.as_ref(),
493            actor.id.as_ref(),
494            resource.type_.as_ref(),
495            resource.id.as_ref(),
496        ) else {
497            if actor.type_.is_none() || actor.id.is_none() {
498                return Err(Error::Input(
499                    "Actor must be a concrete value. Try `oso.query` if you want to get all permitted actors"
500                        .to_owned(),
501                ));
502            }
503            if resource.type_.is_none() || resource.id.is_none() {
504                return Err(Error::Input("Resource must be a concrete value. Try `oso.query` if you want to get all allowed actions and resources".to_owned()));
505            }
506            unreachable!();
507        };
508
509        let body = ActionsRequest {
510            actor_type,
511            actor_id,
512            resource_type,
513            resource_id,
514            context_facts: &self.context,
515        };
516
517        let resp: ActionsResponse = self.client.post("actions", &body, false).await?;
518        Ok(resp.results)
519    }
520
521    pub async fn list(
522        &self,
523        actor: impl Into<Value<'_>>,
524        action: &str,
525        resource_type: &str,
526    ) -> Result<Vec<String>, Error> {
527        #[derive(Debug, Serialize)]
528        struct ListRequest<'a> {
529            actor_type: &'a str,
530            actor_id: &'a str,
531            action: &'a str,
532            resource_type: &'a str,
533            context_facts: &'a Vec<Fact<'a>>,
534        }
535
536        #[derive(Deserialize)]
537        struct ListResponse {
538            results: Vec<String>,
539        }
540
541        let actor = actor.into();
542
543        let (Some(actor_type), Some(actor_id)) = (actor.type_.as_ref(), actor.id.as_ref()) else {
544            return Err(Error::Input(
545                "Actor must be a concrete value. Try `oso.query` if you want to get all permitted actors".to_owned(),
546            ));
547        };
548
549        let body = ListRequest {
550            actor_type,
551            actor_id,
552            action,
553            resource_type,
554            context_facts: &self.context,
555        };
556
557        let resp: ListResponse = self.client.post("list", &body, false).await?;
558        Ok(resp.results)
559    }
560
561    /// Query Oso Cloud for any predicate and any combination of concrete and wildcard arguments.
562    ///
563    /// Unlike `oso.get`, which only lists facts you've added, you can use `oso.query` to list
564    /// derived information about any rule in your policy.
565    pub async fn query(&self, fact: &Fact<'_>) -> Result<Vec<Fact<'static>>, Error> {
566        #[derive(Debug, Serialize)]
567        struct QueryRequest<'a> {
568            fact: &'a Fact<'a>,
569            context_facts: &'a Vec<Fact<'a>>,
570        }
571
572        let body = QueryRequest {
573            fact,
574            context_facts: &self.context,
575        };
576
577        #[derive(Deserialize)]
578        struct QueryResponse {
579            results: Vec<Fact<'static>>,
580        }
581
582        let resp: QueryResponse = self.client.post("query", &body, false).await?;
583
584        Ok(resp.results)
585    }
586}
587#[cfg(test)]
588mod tests {}