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#[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 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 pub fn any() -> Self {
81 Self { type_: None, id: None }
82 }
83
84 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 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#[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 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 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 pub async fn tell(&self, fact: Fact<'_>) -> Result<(), Error> {
238 self.bulk(&[], &[fact]).await
239 }
240
241 pub async fn delete(&self, fact: Fact<'_>) -> Result<(), Error> {
243 self.bulk(&[fact], &[]).await
244 }
245
246 pub async fn bulk_tell(&self, facts: &[Fact<'_>]) -> Result<(), Error> {
248 self.bulk(&[], facts).await
249 }
250
251 pub async fn bulk_delete(&self, facts: &[Fact<'_>]) -> Result<(), Error> {
256 self.bulk(facts, &[]).await
257 }
258
259 pub async fn bulk(&self, delete: &[Fact<'_>], tell: &[Fact<'_>]) -> Result<(), Error> {
263 self.client.bulk(delete, tell).await
264 }
265
266 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 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 return Ok(());
446 }
447
448 let mut results_iter = resp.results.into_iter();
449 let mut next = results_iter.next();
450
451 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 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 {}