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#[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 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 pub fn any() -> Self {
82 Self { type_: None, id: None }
83 }
84
85 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 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#[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 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 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 pub async fn tell(&self, fact: Fact<'_>) -> Result<(), Error> {
259 self.bulk(&[], &[fact]).await
260 }
261
262 pub async fn delete(&self, fact: Fact<'_>) -> Result<(), Error> {
264 self.bulk(&[fact], &[]).await
265 }
266
267 pub async fn bulk_tell(&self, facts: &[Fact<'_>]) -> Result<(), Error> {
269 self.bulk(&[], facts).await
270 }
271
272 pub async fn bulk_delete(&self, facts: &[Fact<'_>]) -> Result<(), Error> {
277 self.bulk(facts, &[]).await
278 }
279
280 pub async fn bulk(&self, delete: &[Fact<'_>], tell: &[Fact<'_>]) -> Result<(), Error> {
284 self.client.bulk(delete, tell).await
285 }
286
287 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 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 return Ok(());
467 }
468
469 let mut results_iter = resp.results.into_iter();
470 let mut next = results_iter.next();
471
472 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 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 {}