diff --git a/languages/rust/oso/src/errors.rs b/languages/rust/oso/src/errors.rs index 55f46730ac..e4be204ab5 100644 --- a/languages/rust/oso/src/errors.rs +++ b/languages/rust/oso/src/errors.rs @@ -1,27 +1,41 @@ +//! # Error types used by the Oso library. +//! +//! This module contains a collection of error types that can be returned by various calls by the +//! Oso library. use std::fmt; - use thiserror::Error; +/// Errors returned by the Polar library. pub use polar_core::error as polar; // TODO stack traces???? -/// oso errors +/// Oso error type. /// -/// TODO: fill in other variants +/// This enum encompasses all things that can go wrong while using the Oso library. It can also be +/// used to wrap a custom error message, using the [`OsoError::Custom`] variant or using the +/// [`lazy_error`](crate::lazy_error) macro. #[allow(clippy::large_enum_variant)] #[derive(Error, Debug)] pub enum OsoError { + /// Input/output error. #[error(transparent)] Io(#[from] std::io::Error), + + /// Polar error, see [`PolarError`](polar::PolarError). #[error(transparent)] Polar(#[from] polar::PolarError), + + /// Failed to convert type from Polar. #[error("failed to convert type from Polar")] FromPolar, + + /// Incorrect file name, must have `.polar` extension. #[error("policy files must have the .polar extension. {filename} does not.")] IncorrectFileType { filename: String }, - #[error("Invariant error: {source}")] + /// Invariant error. + #[error(transparent)] InvariantError { #[from] source: InvariantError, @@ -31,34 +45,43 @@ pub enum OsoError { #[error(transparent)] TypeError(TypeError), + /// Unsupported operation for the given type. #[error("Unsupported operation {operation} for type {type_name}.")] UnsupportedOperation { operation: String, type_name: String, }, + /// Unimplemented operation. #[error("{operation} are unimplemented in the oso Rust library")] UnimplementedOperation { operation: String }, + /// Inline query failed. #[error("Inline query failed {location}")] InlineQueryFailedError { location: String }, + /// Invalid call error. #[error(transparent)] InvalidCallError(#[from] InvalidCallError), + /// Failure converting type to polar. #[error("failed to convert type to Polar")] ToPolar, + /// Class already registered. #[error("Class {name} already registered")] DuplicateClassError { name: String }, + /// Missing class error. #[error("No class called {name} has been registered")] MissingClassError { name: String }, + /// Missing instance error. #[error("Tried to find an instance that doesn't exist -- internal error")] MissingInstanceError, - /// TODO: replace all these with proper variants + // TODO: replace all these with proper variants + /// Custom error. #[error("{message}")] Custom { message: String }, @@ -92,7 +115,7 @@ impl OsoError { } } -/// These are conditions that should never occur, and indicate a bug in oso. +/// These are conditions that should never occur, and indicate a bug in Oso. #[derive(Error, Debug)] pub enum InvariantError { #[error("Invalid receiver for method. {0}")] @@ -102,9 +125,14 @@ pub enum InvariantError { MethodNotFound, } +/// Type error +/// +/// This error results from using the wrong type in a place where a specific type is expected. #[derive(Error, Debug)] pub struct TypeError { + /// Type that was received pub got: Option, + /// Type that was expected pub expected: String, } @@ -146,6 +174,10 @@ impl TypeError { } } +/// Invalid call error. +/// +/// Generated when an invalid call is encountered, such as calling a method or attribute on a class +/// that does exist. #[derive(Error, Debug)] pub enum InvalidCallError { #[error("Class method {method_name} not found on type {type_name}.")] @@ -165,4 +197,8 @@ pub enum InvalidCallError { }, } -pub type Result = std::result::Result; +/// Convenience wrapper for Oso results. +/// +/// This is the same as the standard library [`Result`], except that it defaults to [`OsoError`] as +/// the error type. +pub type Result = std::result::Result; diff --git a/languages/rust/oso/src/extras.rs b/languages/rust/oso/src/extras.rs index 244cb8e9f2..64461508c0 100644 --- a/languages/rust/oso/src/extras.rs +++ b/languages/rust/oso/src/extras.rs @@ -1,26 +1,23 @@ +#[allow(unused)] +use crate::host::{Class, ClassBuilder}; + #[cfg(feature = "uuid-06")] impl crate::PolarClass for uuid_06::Uuid { - fn get_polar_class_builder() -> crate::host::ClassBuilder { - crate::host::Class::builder() - .name("Uuid") - .with_equality_check() + fn get_polar_class_builder() -> ClassBuilder { + Class::builder().name("Uuid").with_equality_check() } } #[cfg(feature = "uuid-07")] impl crate::PolarClass for uuid_07::Uuid { - fn get_polar_class_builder() -> crate::host::ClassBuilder { - crate::host::Class::builder() - .name("Uuid") - .with_equality_check() + fn get_polar_class_builder() -> ClassBuilder { + Class::builder().name("Uuid").with_equality_check() } } #[cfg(feature = "uuid-10")] impl crate::PolarClass for uuid_10::Uuid { - fn get_polar_class_builder() -> crate::host::ClassBuilder { - crate::host::Class::builder() - .name("Uuid") - .with_equality_check() + fn get_polar_class_builder() -> ClassBuilder { + Class::builder().name("Uuid").with_equality_check() } } diff --git a/languages/rust/oso/src/host/from_polar.rs b/languages/rust/oso/src/host/from_polar.rs index 2b3caaf391..1537bddf10 100644 --- a/languages/rust/oso/src/host/from_polar.rs +++ b/languages/rust/oso/src/host/from_polar.rs @@ -2,15 +2,14 @@ //! Trait and implementations of `FromPolar` for converting from //! Polar types back to Rust types. -use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque}; -use std::hash::Hash; - +use super::{class::Instance, PolarValue}; +use crate::{errors::TypeError, OsoError, PolarClass, Result}; use impl_trait_for_tuples::*; - -use super::class::Instance; -use super::PolarValue; -use crate::errors::TypeError; -use crate::PolarClass; +use std::{ + collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque}, + hash::Hash, +}; +use tracing::{trace, warn}; /// Convert Polar types to Rust types. /// @@ -32,11 +31,11 @@ use crate::PolarClass; /// is possible to store a `FromPolar` value on an `Oso` instance /// which can be shared between threads pub trait FromPolar: Clone { - fn from_polar(val: PolarValue) -> crate::Result; + fn from_polar(val: PolarValue) -> Result; } impl FromPolar for PolarValue { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { Ok(val) } } @@ -44,9 +43,9 @@ impl FromPolar for PolarValue { macro_rules! polar_to_int { ($i:ty) => { impl FromPolar for $i { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::Integer(i) = val { - <$i>::try_from(i).map_err(|_| crate::OsoError::FromPolar) + <$i>::try_from(i).map_err(|_| OsoError::FromPolar) } else { Err(TypeError::expected("Integer").user()) } @@ -67,7 +66,7 @@ impl FromPolar for T where T: 'static + Clone + PolarClass, { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::Instance(instance) = val { Ok(instance.downcast::(None).map_err(|e| e.user())?.clone()) } else { @@ -77,7 +76,7 @@ where } impl FromPolar for f64 { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::Float(f) = val { Ok(f) } else { @@ -87,7 +86,7 @@ impl FromPolar for f64 { } impl FromPolar for String { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::String(s) = val { Ok(s) } else { @@ -97,7 +96,7 @@ impl FromPolar for String { } impl FromPolar for bool { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::Boolean(b) = val { Ok(b) } else { @@ -107,7 +106,7 @@ impl FromPolar for bool { } impl FromPolar for HashMap { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::Map(map) = val { let mut result = HashMap::new(); for (k, v) in map { @@ -122,7 +121,7 @@ impl FromPolar for HashMap { } impl FromPolar for BTreeMap { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::Map(map) = val { let mut result = BTreeMap::new(); for (k, v) in map { @@ -137,7 +136,7 @@ impl FromPolar for BTreeMap { } impl FromPolar for Vec { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::List(l) = val { let mut result = vec![]; for v in l { @@ -151,7 +150,7 @@ impl FromPolar for Vec { } impl FromPolar for LinkedList { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::List(l) = val { let mut result = LinkedList::new(); for v in l { @@ -165,7 +164,7 @@ impl FromPolar for LinkedList { } impl FromPolar for VecDeque { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::List(l) = val { let mut result = VecDeque::new(); for v in l { @@ -179,7 +178,7 @@ impl FromPolar for VecDeque { } impl FromPolar for HashSet { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::List(l) = val { let mut result = HashSet::new(); for v in l { @@ -193,7 +192,7 @@ impl FromPolar for HashSet { } impl FromPolar for BTreeSet { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::List(l) = val { let mut result = BTreeSet::new(); for v in l { @@ -207,7 +206,7 @@ impl FromPolar for BTreeSet { } impl FromPolar for BinaryHeap { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if let PolarValue::List(l) = val { let mut result = BinaryHeap::new(); for v in l { @@ -221,7 +220,7 @@ impl FromPolar for BinaryHeap { } impl FromPolar for Option { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { // if the value is a Option, convert from PolarValue if let PolarValue::Instance(ref instance) = &val { if let Ok(opt) = instance.downcast::>(None) { @@ -234,7 +233,7 @@ impl FromPolar for Option { // well, you can't do this // impl TryFrom for PolarValue { -// type Error = crate::OsoError; +// type Error = OsoError; // fn try_from(v: PolarValue) -> Result { // U::from_polar(v) @@ -245,7 +244,7 @@ impl FromPolar for Option { macro_rules! try_from_polar { ($i:ty) => { impl TryFrom for $i { - type Error = crate::OsoError; + type Error = OsoError; fn try_from(v: PolarValue) -> Result { Self::from_polar(v) @@ -266,7 +265,7 @@ try_from_polar!(String); try_from_polar!(bool); impl TryFrom for HashMap { - type Error = crate::OsoError; + type Error = OsoError; fn try_from(v: PolarValue) -> Result { Self::from_polar(v) @@ -274,26 +273,15 @@ impl TryFrom for HashMap { } impl TryFrom for Vec { - type Error = crate::OsoError; + type Error = OsoError; fn try_from(v: PolarValue) -> Result { Self::from_polar(v) } } -mod private { - /// Prevents implementations of `FromPolarList` outside of this crate - pub trait Sealed {} -} - -pub trait FromPolarList: private::Sealed { - fn from_polar_list(values: &[PolarValue]) -> crate::Result - where - Self: Sized; -} - impl FromPolar for Instance { - fn from_polar(value: PolarValue) -> crate::Result { + fn from_polar(value: PolarValue) -> Result { // We need to handle converting all value variants to an // instance so that we can use the `Class` mechanism to // handle methods on them @@ -306,34 +294,49 @@ impl FromPolar for Instance { PolarValue::Map(d) => Instance::new(d), PolarValue::Instance(instance) => instance, v => { - tracing::warn!(value = ?v, "invalid conversion attempted"); - return Err(crate::OsoError::FromPolar); + warn!(value = ?v, "invalid conversion attempted"); + return Err(OsoError::FromPolar); } }; Ok(instance) } } +mod private { + /// Prevents implementations of `FromPolarList` outside of this crate + pub trait Sealed {} +} + +/// Convert lists of Polar values into a Rust tuple. +/// +/// This trait is automatically implemented to tuples of up to 16 elements. +pub trait FromPolarList: private::Sealed { + /// Attempt to convert the list of PolarValues into Self. + fn from_polar_list(values: &[PolarValue]) -> Result + where + Self: Sized; +} + #[impl_for_tuples(16)] #[tuple_types_custom_trait_bound(FromPolar)] impl FromPolarList for Tuple { - fn from_polar_list(values: &[PolarValue]) -> crate::Result { + fn from_polar_list(values: &[PolarValue]) -> Result { let mut iter = values.iter(); let result = Ok((for_tuples!( #( Tuple::from_polar(iter.next().ok_or( // TODO better error type - crate::OsoError::FromPolar + OsoError::FromPolar )?.clone())? ),* ))); if iter.len() > 0 { // TODO (dhatch): Debug this!!! - tracing::warn!("Remaining items in iterator after conversion."); + warn!("Remaining items in iterator after conversion."); for item in iter { - tracing::trace!("Remaining item {:?}", item); + trace!("Remaining item {:?}", item); } - return Err(crate::OsoError::FromPolar); + return Err(OsoError::FromPolar); } result diff --git a/languages/rust/oso/src/host/to_polar.rs b/languages/rust/oso/src/host/to_polar.rs index d45fb9ce07..cafb6afb3f 100644 --- a/languages/rust/oso/src/host/to_polar.rs +++ b/languages/rust/oso/src/host/to_polar.rs @@ -8,7 +8,7 @@ use impl_trait_for_tuples::*; use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque}; use super::DEFAULT_CLASSES; -use crate::PolarValue; +use crate::{Class, ClassBuilder, OsoError, PolarClass, PolarValue, Result}; /// Convert Rust types to Polar types. /// @@ -57,7 +57,7 @@ pub trait ToPolar { fn to_polar(self) -> PolarValue; } -impl ToPolar for C { +impl ToPolar for C { fn to_polar(self) -> PolarValue { let registered = DEFAULT_CLASSES .read() @@ -78,19 +78,19 @@ impl ToPolar for C { } pub trait ToPolarResult { - fn to_polar_result(self) -> crate::Result; + fn to_polar_result(self) -> Result; } impl ToPolarResult for R { - fn to_polar_result(self) -> crate::Result { + fn to_polar_result(self) -> Result { Ok(self.to_polar()) } } impl ToPolarResult for Result { - fn to_polar_result(self) -> crate::Result { + fn to_polar_result(self) -> Result { self.map(|r| r.to_polar()) - .map_err(|e| crate::OsoError::ApplicationError { + .map_err(|e| OsoError::ApplicationError { source: Box::new(e), attr: None, type_name: None, @@ -311,7 +311,7 @@ impl PolarIterator { } impl Iterator for PolarIterator { - type Item = crate::Result; + type Item = Result; fn next(&mut self) -> Option { self.0.next() } @@ -322,15 +322,15 @@ impl Clone for PolarIterator { Self(self.0.box_clone()) } } -impl crate::PolarClass for PolarIterator { - fn get_polar_class_builder() -> crate::ClassBuilder { - crate::Class::builder::().with_iter() +impl PolarClass for PolarIterator { + fn get_polar_class_builder() -> ClassBuilder { + Class::builder::().with_iter() } } pub trait PolarResultIter: Send + Sync { fn box_clone(&self) -> Box; - fn next(&mut self) -> Option>; + fn next(&mut self) -> Option>; } impl PolarResultIter for I @@ -342,7 +342,7 @@ where Box::new(self.clone()) } - fn next(&mut self) -> Option> { + fn next(&mut self) -> Option> { Iterator::next(self).map(|v| v.to_polar_result()) } } diff --git a/languages/rust/oso/src/host/value.rs b/languages/rust/oso/src/host/value.rs index 487b8b2c1c..1df08a73ae 100644 --- a/languages/rust/oso/src/host/value.rs +++ b/languages/rust/oso/src/host/value.rs @@ -1,16 +1,16 @@ use polar_core::terms::*; use std::collections::hash_map::HashMap; -use crate::host::{Host, Instance}; +use crate::{ + host::{Host, Instance}, + OsoError, Result, +}; -/// An enum of the possible value types that can be -/// sent to/from Polar. +/// An enum of the possible value types that can be sent to and from Polar. /// -/// All variants except `Instance` represent types that can -/// be used natively in Polar. -/// Any other types can be wrapped using `PolarValue::new_from_instance`. -/// If the instance has a registered `Class`, then this can be used -/// from the policy too. +/// All variants except [`PolarValue::Instance`] represent types that can be used natively in +/// Polar. All other types can be wrapped using [`PolarValue::new_from_instance`]. If the instance +/// has a registered [`Class`](crate::Class), then this can be used from the policy too. #[derive(Clone, Debug)] pub enum PolarValue { Integer(i64), @@ -38,7 +38,10 @@ impl PartialEq for PolarValue { } impl PolarValue { - /// Create a `PolarValue::Instance` from any type. + /// Create a [`PolarValue::Instance`] from any type. + /// + /// The only constraints are that the type must me threadsafe (as represented by the [`Send`] + /// and [`Sync`] bounds), as well as owned (as represented by the `'static` bound). pub fn new_from_instance(instance: T) -> Self where T: Send + Sync + 'static, @@ -46,7 +49,7 @@ impl PolarValue { Self::Instance(Instance::new(instance)) } - pub(crate) fn from_term(term: &Term, host: &Host) -> crate::Result { + pub(crate) fn from_term(term: &Term, host: &Host) -> Result { let val = match term.value() { Value::Number(Numeric::Integer(i)) => PolarValue::Integer(*i), Value::Number(Numeric::Float(f)) => PolarValue::Float(*f), @@ -73,7 +76,7 @@ impl PolarValue { } Value::Variable(Symbol(sym)) => PolarValue::Variable(sym.clone()), Value::Expression(_) => { - return Err(crate::OsoError::Custom { + return Err(OsoError::Custom { message: r#" Received Expression from Polar VM. The Expression type is not yet supported in this language. @@ -83,7 +86,7 @@ This may mean you performed an operation in your policy over an unbound variable }) } _ => { - return Err(crate::OsoError::Custom { + return Err(OsoError::Custom { message: "Unsupported value type".to_owned(), }) } diff --git a/languages/rust/oso/src/lib.rs b/languages/rust/oso/src/lib.rs index 8fdc4d0451..9d7d1060dc 100644 --- a/languages/rust/oso/src/lib.rs +++ b/languages/rust/oso/src/lib.rs @@ -74,7 +74,7 @@ //! #[macro_use] -pub mod macros; +mod macros; pub(crate) mod builtins; pub mod errors; diff --git a/languages/rust/oso/src/macros.rs b/languages/rust/oso/src/macros.rs index cbab97bec5..94a7440cfa 100644 --- a/languages/rust/oso/src/macros.rs +++ b/languages/rust/oso/src/macros.rs @@ -1,3 +1,4 @@ +/// Create a custom [`OsoError`](crate::errors::OsoError), with a syntax similar to `format!()`. #[macro_export] macro_rules! lazy_error { ($($input:tt)*) => { diff --git a/languages/rust/oso/src/oso.rs b/languages/rust/oso/src/oso.rs index 633d626678..291fcf7bde 100644 --- a/languages/rust/oso/src/oso.rs +++ b/languages/rust/oso/src/oso.rs @@ -1,22 +1,52 @@ -//! Communicate with the Polar virtual machine: load rules, make queries, etc/ -use polar_core::sources::Source; -use polar_core::terms::{Call, Symbol, Term, Value}; +//! # Oso module +//! +//! Communicate with the Polar virtual machine: load rules, make queries, etc. +use crate::{ + host::Host, query::Query, Class, FromPolar, OsoError, PolarValue, Result, ToPolar, ToPolarList, +}; +use polar_core::{ + polar::Polar, + sources::Source, + terms::{Call, Symbol, Term, Value}, +}; +use std::{collections::HashSet, fs::File, hash::Hash, io::Read, sync::Arc}; -use std::collections::HashSet; -use std::fs::File; -use std::hash::Hash; -use std::io::Read; -use std::sync::Arc; - -use crate::host::Host; -use crate::query::Query; -use crate::{FromPolar, OsoError, PolarValue, ToPolar, ToPolarList}; - -/// Oso is the main struct you interact with. It is an instance of the Oso authorization library -/// and contains the polar language knowledge base and query engine. +/// Instance of the Oso authorization library. +/// +/// This is the main struct you interact with. Contains the polar language knowledge base and query +/// engine. +/// +/// # Usage +/// +/// Typically, you will create a new instance of Oso, load some definitions into it that you +/// can use to determine authorization and then use [`is_allowed()`](Oso::is_allowed) to check for +/// authorization. +/// +/// ```rust +/// # use oso::Oso; +/// let mut oso = Oso::new(); +/// +/// // allow any actor to perform read on any resource +/// oso.load_str(r#"allow(_actor, "read", _resource);"#).unwrap(); +/// +/// assert!(oso.is_allowed("me", "read", "book").unwrap()); +/// ``` +/// +/// To make Oso more useful to you, you can augment it with your own custom Rust types by using the +/// [`register_class()`](Oso::register_class) method. +/// +/// Besides only checking if something is allowed, Oso can also tell you all of the actions that an +/// actor may take on a resource using the [`get_allowed_actions()`](Oso::get_allowed_actions) +/// method. +/// +/// # Quickstart +/// +/// You can check out the [Quickstart +/// guide](https://docs.osohq.com/rust/getting-started/quickstart.html) for more information on how +/// to get started using Oso. #[derive(Clone)] pub struct Oso { - inner: Arc, + inner: Arc, host: Host, } @@ -40,7 +70,7 @@ pub enum Action { } impl FromPolar for Action { - fn from_polar(val: PolarValue) -> crate::Result { + fn from_polar(val: PolarValue) -> Result { if matches!(val, PolarValue::Variable(_)) { Ok(Action::Any) } else { @@ -50,7 +80,18 @@ impl FromPolar for Action { } impl Oso { - /// Create a new instance of Oso. Each instance is separate and can have different rules and classes loaded into it. + /// Create a new instance of Oso. + /// + /// Each instance is separate and can have different rules and classes loaded into it. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// # use oso::Oso; + /// let oso = Oso::new(); + /// ``` pub fn new() -> Self { let inner = Arc::new(polar_core::polar::Polar::new()); let host = Host::new(inner.clone()); @@ -61,18 +102,36 @@ impl Oso { oso.register_class(class) .expect("failed to register builtin class"); } - oso.register_constant(Option::::None, "nil") + oso.register_constant(Option::::None, "nil") .expect("failed to register the constant None"); oso } - /// High level interface for authorization decisions. Makes an allow query with the given actor, action and resource and returns true or false. + /// Test if an `actor` is allowed to perform an `action` on a `resource`. + /// + /// High level interface for authorization decisions. Makes an allow query with the given + /// actor, action and resource and returns `true` or `false`. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// # use oso::Oso; + /// let mut oso = Oso::new(); + /// + /// // allow any actor to perform read on any resource + /// oso.load_str(r#"allow(_actor, "read", _resource);"#).unwrap(); + /// + /// assert_eq!(oso.is_allowed("me", "read", "book").unwrap(), true); + /// assert_eq!(oso.is_allowed("me", "steal", "book").unwrap(), false); + /// ``` pub fn is_allowed( &self, actor: Actor, action: Action, resource: Resource, - ) -> crate::Result + ) -> Result where Actor: ToPolar, Action: ToPolar, @@ -87,23 +146,77 @@ impl Oso { } /// Get the actions actor is allowed to take on resource. - /// Returns a [std::collections::HashSet] of actions, typed according the return value. + /// + /// Returns a [`HashSet`] of actions, typed according the return value. It can return + /// [`Action`] structs, which can encode that an actor can do anything with [`Action::Any`], or + /// any type that implements [`FromPolar`]. + /// /// # Examples - /// ```ignore - /// oso.load_str(r#"allow(actor: Actor{name: "sally"}, action, resource: Widget{id: 1}) if - /// action in ["CREATE", "READ"];"#); /// - /// // get a HashSet of oso::Actions - /// let actions: HashSet = oso.get_allowed_actions(actor, resource)?; + /// Basic usage: + /// + /// ```rust + /// # use oso::{Action, Oso}; + /// # use std::collections::HashSet; + /// let mut oso = Oso::new(); + /// + /// // anyone can read anything, and thomas can drive and sell a car. + /// oso.load_str(r#" + /// allow(_actor, "read", "book"); + /// allow("thomas", action, "car") if action in ["drive", "sell"]; + /// "#).unwrap(); + /// + /// // anyone can read a book + /// let actions: HashSet = oso.get_allowed_actions("thomas", "book").unwrap(); + /// assert_eq!(actions, [Action::Typed("read".into())].into()); + /// + /// // only thomas can drive and sell his car + /// let actions: HashSet = oso.get_allowed_actions("thomas", "car").unwrap(); + /// assert_eq!(actions, [ + /// "drive".into(), + /// "sell".into() + /// ].into()); + /// ``` + /// + /// If you prefer not to use "stringly-typed" actions, you can define your own action type. + /// This method even works when using that action type. Here is an example, where an enum is + /// used as an action type: + /// + /// ```rust + /// # use oso::{Action, Oso, PolarClass}; + /// # use std::collections::HashSet; + /// let mut oso = Oso::new(); + /// + /// #[derive(PolarClass, PartialEq, Clone, Debug, Eq, Hash)] + /// enum MyAction { + /// Read, + /// Write, + /// } + /// + /// oso.register_class( + /// MyAction::get_polar_class_builder() + /// .with_equality_check() + /// .add_constant(MyAction::Read, "Read") + /// .add_constant(MyAction::Write, "Write") + /// .build() + /// ).unwrap(); + /// + /// oso.load_str(r#" + /// allow("backend", MyAction::Read, _resource); + /// allow("backend", MyAction::Write, "database"); + /// "#).unwrap(); /// - /// // or Strings - /// let actions: HashSet = oso.get_allowed_actions(actor, resource)?; + /// let actions: HashSet = oso.get_allowed_actions("backend", "database").unwrap(); + /// assert_eq!(actions, [ + /// MyAction::Read, + /// MyAction::Write, + /// ].into()); /// ``` pub fn get_allowed_actions( &self, actor: Actor, resource: Resource, - ) -> crate::Result> + ) -> Result> where Actor: ToPolar, Resource: ToPolar, @@ -133,17 +246,32 @@ impl Oso { } /// Clear out all files and rules that have been loaded. - pub fn clear_rules(&mut self) -> crate::Result<()> { + /// + /// This message may return an error. + /// + /// # Examples + /// + /// ```rust + /// # use oso::Oso; + /// let mut oso = Oso::new(); + /// + /// // load some rules + /// oso.load_str("allow(_actor, _action, _resource) if true;").unwrap(); + /// + /// // clear rules + /// oso.clear_rules().unwrap(); + /// ``` + pub fn clear_rules(&mut self) -> Result<()> { self.inner.clear_rules(); check_messages!(self.inner); Ok(()) } - fn check_inline_queries(&self) -> crate::Result<()> { + fn check_inline_queries(&self) -> Result<()> { while let Some(q) = self.inner.next_inline_query(false) { let location = q.source_info(); let query = Query::new(q, self.host.clone()); - match query.collect::>>() { + match query.collect::>>() { Ok(v) if !v.is_empty() => continue, Ok(_) => return Err(OsoError::InlineQueryFailedError { location }), Err(e) => return lazy_error!("error in inline query: {}", e), @@ -154,7 +282,7 @@ impl Oso { } // Register MROs, load Polar code, and check inline queries. - fn load_sources(&mut self, sources: Vec) -> crate::Result<()> { + fn load_sources(&mut self, sources: Vec) -> Result<()> { self.host.register_mros()?; self.inner.load(sources)?; self.check_inline_queries() @@ -163,17 +291,23 @@ impl Oso { /// Load a file containing Polar rules. All Polar files must end in `.polar`. #[deprecated( since = "0.20.1", - note = "`Oso::load_file` has been deprecated in favor of `Oso::load_files` as of the 0.20 release.\n\nPlease see changelog for migration instructions: https://docs.osohq.com/project/changelogs/2021-09-15.html" + note = "`Oso::load_file` has been deprecated in favor of `Oso::load_files` as of the 0.20 release. Please see changelog for migration instructions: " )] - pub fn load_file>(&mut self, filename: P) -> crate::Result<()> { + pub fn load_file>(&mut self, filename: P) -> Result<()> { self.load_files(vec![filename]) } - /// Load files containing Polar rules. All Polar files must end in `.polar`. - pub fn load_files>( - &mut self, - filenames: Vec

, - ) -> crate::Result<()> { + /// Load files containing Polar rules. + /// + /// All Polar files must have the `.polar` extension. + /// + /// ```rust + /// # use oso::Oso; + /// let mut oso = Oso::new(); + /// + /// oso.load_files(vec!["../test.polar"]).unwrap(); + /// ``` + pub fn load_files>(&mut self, filenames: Vec

) -> Result<()> { if filenames.is_empty() { return Ok(()); } @@ -184,7 +318,7 @@ impl Oso { let file = file.as_ref(); let filename = file.to_string_lossy().into_owned(); if !file.extension().map_or(false, |ext| ext == "polar") { - return Err(crate::OsoError::IncorrectFileType { filename }); + return Err(OsoError::IncorrectFileType { filename }); } let mut f = File::open(file)?; let mut src = String::new(); @@ -196,21 +330,48 @@ impl Oso { } /// Load a string of polar source directly. + /// /// # Examples - /// ```ignore - /// oso.load_str("allow(a, b, c) if true;"); + /// + /// Basic usage: + /// + /// ```rust + /// # use oso::Oso; + /// let mut oso = Oso::new(); + /// + /// oso.load_str("allow(_actor, _action, _resource) if true;").unwrap(); + /// ``` + /// + /// Loading a source that is baked into the binary at compile-time: + /// + /// ```rust + /// # use oso::Oso; + /// let mut oso = Oso::new(); + /// + /// oso.load_str(include_str!("../test.polar")).unwrap(); /// ``` - pub fn load_str(&mut self, src: &str) -> crate::Result<()> { + pub fn load_str(&mut self, src: &str) -> Result<()> { // TODO(gj): emit... some sort of warning? self.load_sources(vec![Source::new(src)]) } - /// Query the knowledge base. This can be an allow query or any other polar expression. + /// Query the knowledge base. + /// + /// This can be an allow query (like `allow(actor, "read", resource) or any other Polar expression. + /// /// # Examples - /// ```ignore - /// oso.query("x = 1 or x = 2"); + /// + /// Basic usage: + /// + /// ```rust + /// # use oso::Oso; + /// let mut oso = Oso::new(); + /// + /// oso.register_constant(2, "x").unwrap(); + /// + /// oso.query("x = 1 or x = 2").unwrap(); /// ``` - pub fn query(&self, s: &str) -> crate::Result { + pub fn query(&self, s: &str) -> Result { let query = self.inner.new_query(s, false)?; check_messages!(self.inner); let query = Query::new(query, self.host.clone()); @@ -224,7 +385,7 @@ impl Oso { /// oso.query_rule("is_admin", vec![User{name: "steve"}]); /// ``` #[must_use = "Query that is not consumed does nothing."] - pub fn query_rule(&self, name: &str, args: impl ToPolarList) -> crate::Result { + pub fn query_rule(&self, name: &str, args: impl ToPolarList) -> Result { let mut query_host = self.host.clone(); let args = args .to_polar_list() @@ -244,8 +405,48 @@ impl Oso { } /// Register a rust type as a Polar class. - /// See [`oso::Class`] docs. - pub fn register_class(&mut self, class: crate::host::Class) -> crate::Result<()> { + /// + /// See also the [`Class`] docs. + /// + /// Typically, you can simply derive [`PolarClass`] for your custom types and then use the + /// [`get_polar_class()`](crate::PolarClass::get_polar_class) method. + /// + /// # Examples + /// + /// ```rust + /// # use oso::{PolarClass, Oso}; + /// #[derive(PolarClass)] + /// struct MyClass; + /// + /// let mut oso = Oso::new(); + /// + /// oso.register_class(MyClass::get_polar_class()); + /// ``` + /// + /// In order to customize your class for Polar, you can use the methods of + /// [`ClassBuilder`](crate::ClassBuilder). + /// + /// ```rust + /// # use oso::{PolarClass, Oso}; + /// #[derive(PolarClass, PartialEq, Clone, Debug, Eq, Hash)] + /// enum Service { + /// Database, + /// Frontend, + /// Backend, + /// } + /// + /// let class = Service::get_polar_class_builder() + /// .with_equality_check() + /// .add_constant(Service::Frontend, "Frontend") + /// .add_constant(Service::Backend, "Backend") + /// .add_constant(Service::Database, "Database") + /// .build(); + /// + /// let mut oso = Oso::new(); + /// + /// oso.register_class(class).unwrap(); + /// ``` + pub fn register_class(&mut self, class: Class) -> Result<()> { let name = class.name.clone(); let class_name = self.host.cache_class(class.clone(), name)?; @@ -255,13 +456,25 @@ impl Oso { self.register_constant(class, &class_name) } - /// Register a rust type as a Polar constant. - /// See [`oso::Class`] docs. - pub fn register_constant( + /// Register a Rust value as a Polar constant. + /// + /// The Rust value must implement [`ToPolar`]. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// # use oso::Oso; + /// let mut oso = Oso::new(); + /// + /// oso.register_constant(std::f64::consts::PI, "PI"); + /// ``` + pub fn register_constant( &mut self, value: V, name: &str, - ) -> crate::Result<()> { + ) -> Result<()> { self.inner.register_constant( Symbol(name.to_string()), value.to_polar().to_term(&mut self.host), diff --git a/languages/rust/oso/src/query.rs b/languages/rust/oso/src/query.rs index 91b5849198..5e3d40c23c 100644 --- a/languages/rust/oso/src/query.rs +++ b/languages/rust/oso/src/query.rs @@ -1,29 +1,33 @@ -use std::collections::BTreeMap; -use std::collections::HashMap; - -use crate::errors::OsoError; -use crate::host::{Host, Instance, PolarIterator}; -use crate::{FromPolar, PolarValue}; - -use polar_core::events::*; -use polar_core::terms::*; +use crate::{ + errors::OsoError, + host::{Host, Instance, PolarIterator}, + FromPolar, PolarValue, Result, +}; +use polar_core::{events::*, kb::Bindings, query::Query as PolarQuery, terms::*}; +use std::collections::{BTreeMap, HashMap}; +use tracing::{debug, error, trace}; impl Iterator for Query { - type Item = crate::Result; + type Item = Result; fn next(&mut self) -> Option { Query::next_result(self) } } +/// Query that can be run against the rules loaded into Oso. +/// +/// This is usually not used directly, but rather through [`Oso::query`](crate::Oso::query) or +/// [`Oso::query_rule`](crate::Oso::query_rule). pub struct Query { - inner: polar_core::query::Query, + inner: PolarQuery, /// Stores a map from call_id to the iterator the call iterates through iterators: HashMap, host: Host, } impl Query { - pub fn new(inner: polar_core::query::Query, host: Host) -> Self { + /// Create a new query. + pub fn new(inner: PolarQuery, host: Host) -> Self { Self { iterators: HashMap::new(), inner, @@ -31,11 +35,13 @@ impl Query { } } + /// Source of the query. pub fn source(&self) -> String { self.inner.source_info() } - pub fn next_result(&mut self) -> Option> { + /// Fetch the next result from this query. + pub fn next_result(&mut self) -> Option> { loop { let event = self.inner.next()?; check_messages!(self.inner); @@ -43,7 +49,7 @@ impl Query { return Some(Err(e.into())); } let event = event.unwrap(); - tracing::debug!(event=?event); + debug!(event=?event); let result = match event { QueryEvent::None => Ok(()), QueryEvent::Done { .. } => return None, @@ -97,7 +103,7 @@ impl Query { match result { // Only call errors get passed back. Err(call_error @ OsoError::InvalidCallError { .. }) => { - tracing::error!("application invalid call error {}", call_error); + error!("application invalid call error {}", call_error); if let Err(e) = self.application_error(call_error) { return Some(Err(e)); } @@ -110,17 +116,17 @@ impl Query { } } - fn question_result(&mut self, call_id: u64, result: bool) -> crate::Result<()> { + fn question_result(&mut self, call_id: u64, result: bool) -> Result<()> { Ok(self.inner.question_result(call_id, result)?) } - fn call_result(&mut self, call_id: u64, result: PolarValue) -> crate::Result<()> { + fn call_result(&mut self, call_id: u64, result: PolarValue) -> Result<()> { Ok(self .inner .call_result(call_id, Some(result.to_term(&mut self.host)))?) } - fn call_result_none(&mut self, call_id: u64) -> crate::Result<()> { + fn call_result_none(&mut self, call_id: u64) -> Result<()> { Ok(self.inner.call_result(call_id, None)?) } @@ -130,11 +136,11 @@ impl Query { /// TODO (dhatch): Refactor Polar API so this is clear. /// /// All other errors must be returned directly from query. - fn application_error(&mut self, error: crate::OsoError) -> crate::Result<()> { + fn application_error(&mut self, error: OsoError) -> Result<()> { Ok(self.inner.application_error(error.to_string())?) } - fn handle_make_external(&mut self, instance_id: u64, constructor: Term) -> crate::Result<()> { + fn handle_make_external(&mut self, instance_id: u64, constructor: Term) -> Result<()> { match constructor.value() { Value::Call(Call { name, args, kwargs }) => { if !kwargs.is_none() { @@ -143,7 +149,7 @@ impl Query { let args = args .iter() .map(|term| PolarValue::from_term(term, &self.host)) - .collect::>>()?; + .collect::>>()?; self.host.make_instance(&name.0, args, instance_id) } } @@ -151,11 +157,11 @@ impl Query { } } - fn next_call_result(&mut self, call_id: u64) -> Option> { + fn next_call_result(&mut self, call_id: u64) -> Option> { self.iterators.get_mut(&call_id).and_then(|c| c.next()) } - fn handle_next_external(&mut self, call_id: u64, iterable: Term) -> crate::Result<()> { + fn handle_next_external(&mut self, call_id: u64, iterable: Term) -> Result<()> { if self.iterators.get(&call_id).is_none() { let iterable_instance = Instance::from_polar(PolarValue::from_term(&iterable, &self.host)?)?; @@ -180,17 +186,17 @@ impl Query { name: Symbol, args: Option>, kwargs: Option>, - ) -> crate::Result<()> { + ) -> Result<()> { if kwargs.is_some() { return lazy_error!("Invalid call error: kwargs not supported in Rust."); } - tracing::trace!(call_id, name = %name, args = ?args, "call"); + trace!(call_id, name = %name, args = ?args, "call"); let instance = Instance::from_polar(PolarValue::from_term(&instance, &self.host)?)?; let result = if let Some(args) = args { let args = args .iter() .map(|v| PolarValue::from_term(v, &self.host)) - .collect::>>()?; + .collect::>>()?; instance.call(&name.0, args, &mut self.host) } else { instance.get_attr(&name.0, &mut self.host) @@ -209,7 +215,7 @@ impl Query { call_id: u64, operator: Operator, args: Vec, - ) -> crate::Result<()> { + ) -> Result<()> { assert_eq!(args.len(), 2); let res = { let args = [ @@ -227,8 +233,8 @@ impl Query { call_id: u64, instance: Term, class_tag: Symbol, - ) -> crate::Result<()> { - tracing::debug!(instance = ?instance, class = %class_tag, "isa"); + ) -> Result<()> { + debug!(instance = ?instance, class = %class_tag, "isa"); let res = self .host .isa(PolarValue::from_term(&instance, &self.host)?, &class_tag.0)?; @@ -242,7 +248,7 @@ impl Query { instance_id: u64, left_class_tag: Symbol, right_class_tag: Symbol, - ) -> crate::Result<()> { + ) -> Result<()> { let res = self .host .is_subspecializer(instance_id, &left_class_tag.0, &right_class_tag.0); @@ -255,31 +261,30 @@ impl Query { call_id: u64, left_class_tag: Symbol, right_class_tag: Symbol, - ) -> crate::Result<()> { + ) -> Result<()> { let res = left_class_tag == right_class_tag; self.question_result(call_id, res)?; Ok(()) } #[allow(clippy::unnecessary_wraps)] - fn handle_debug(&mut self, message: String) -> crate::Result<()> { + fn handle_debug(&mut self, message: String) -> Result<()> { eprintln!("TODO: {}", message); check_messages!(self.inner); Ok(()) } } +/// Set of results from Oso query. #[derive(Clone)] pub struct ResultSet { - bindings: polar_core::kb::Bindings, - host: crate::host::Host, + bindings: Bindings, + host: Host, } impl ResultSet { - pub fn from_bindings( - bindings: polar_core::kb::Bindings, - host: crate::host::Host, - ) -> crate::Result { + /// Create new ResultSet from bindings. + pub fn from_bindings(bindings: Bindings, host: Host) -> Result { // Check for expression. for term in bindings.values() { if term.as_expression().is_ok() && !host.accept_expression { @@ -302,26 +307,31 @@ This may mean you performed an operation in your policy over an unbound variable Box::new(self.bindings.keys().map(|sym| sym.0.as_ref())) } + /// Iterator over the bindings of this result. pub fn iter_bindings(&self) -> Box + '_> { Box::new(self.bindings.iter().map(|(k, v)| (k.0.as_ref(), v.value()))) } + /// Check if the bindings in this result are empty. pub fn is_empty(&self) -> bool { self.bindings.is_empty() } + /// Get a value from the bindings. pub fn get(&self, name: &str) -> Option { self.bindings .get(&Symbol(name.to_string())) .map(|t| PolarValue::from_term(t, &self.host).unwrap()) } - pub fn get_typed(&self, name: &str) -> crate::Result { + /// Get a value from the bindings, and decode them into a Rust type. + pub fn get_typed(&self, name: &str) -> Result { self.get(name) - .ok_or(crate::OsoError::FromPolar) + .ok_or(OsoError::FromPolar) .and_then(T::from_polar) } + /// Turn self into an event. pub fn into_event(self) -> ResultEvent { ResultEvent::new(self.bindings) } @@ -333,9 +343,7 @@ impl std::fmt::Debug for ResultSet { } } -impl, T: crate::host::FromPolar + PartialEq> PartialEq> - for ResultSet -{ +impl, T: FromPolar + PartialEq> PartialEq> for ResultSet { fn eq(&self, other: &HashMap) -> bool { other.iter().all(|(k, v)| { self.get_typed::(k.as_ref())