oso_cloud/
lib.rs

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