diff --git a/biscuit-auth/src/error.rs b/biscuit-auth/src/error.rs index 35856121..bb859b9c 100644 --- a/biscuit-auth/src/error.rs +++ b/biscuit-auth/src/error.rs @@ -260,6 +260,8 @@ pub enum RunLimit { TooManyIterations, #[error("spent too much time verifying")] Timeout, + #[error("Unexpected query results, expected {0} got {1}")] + UnexpectedQueryResult(usize, usize), } #[cfg(test)] diff --git a/biscuit-auth/src/token/authorizer.rs b/biscuit-auth/src/token/authorizer.rs index bed6eb6e..681feed9 100644 --- a/biscuit-auth/src/token/authorizer.rs +++ b/biscuit-auth/src/token/authorizer.rs @@ -445,6 +445,40 @@ impl Authorizer { self.query_with_limits(rule, limits) } + /// Run a query over the authorizer's Datalog engine to gather data. + /// If there is more than one result, this function will throw an error. + /// + /// ```rust + /// # use biscuit_auth::KeyPair; + /// # use biscuit_auth::Biscuit; + /// let keypair = KeyPair::new(); + /// let mut builder = Biscuit::builder(); + /// builder.add_fact("user(\"John Doe\", 42)"); + /// + /// let biscuit = builder.build(&keypair).unwrap(); + /// + /// let mut authorizer = biscuit.authorizer().unwrap(); + /// let res: (String, i64) = authorizer.query_exactly_one("data($name, $id) <- user($name, $id)").unwrap(); + /// assert_eq!(res.0, "John Doe"); + /// assert_eq!(res.1, 42); + /// ``` + pub fn query_exactly_one, T: TryFrom, E: Into>( + &mut self, + rule: R, + ) -> Result + where + error::Token: From<>::Error>, + { + let mut res: Vec = self.query(rule)?; + if res.len() == 1 { + Ok(res.remove(0)) + } else { + Err(error::Token::RunLimit( + error::RunLimit::UnexpectedQueryResult(1, res.len()), + )) + } + } + /// run a query over the authorizer's Datalog engine to gather data /// /// this only sees facts from the authorizer and the authority block @@ -1518,6 +1552,61 @@ mod tests { assert_eq!(res[0].0, "John Doe"); } + #[test] + fn query_exactly_one_authorizer_from_token_string() { + use crate::Biscuit; + use crate::KeyPair; + let keypair = KeyPair::new(); + let mut builder = Biscuit::builder(); + builder.add_fact("user(\"John Doe\")").unwrap(); + + let biscuit = builder.build(&keypair).unwrap(); + + let mut authorizer = biscuit.authorizer().unwrap(); + let res: (String,) = authorizer + .query_exactly_one("data($name) <- user($name)") + .unwrap(); + assert_eq!(res.0, "John Doe"); + } + + #[test] + fn query_exactly_one_no_results() { + use crate::Biscuit; + use crate::KeyPair; + let keypair = KeyPair::new(); + let builder = Biscuit::builder(); + + let biscuit = builder.build(&keypair).unwrap(); + + let mut authorizer = biscuit.authorizer().unwrap(); + let res: Result<(String,), error::Token> = + authorizer.query_exactly_one("data($name) <- user($name)"); + assert_eq!( + res.unwrap_err().to_string(), + "Reached Datalog execution limits" + ); + } + + #[test] + fn query_exactly_one_too_many_results() { + use crate::Biscuit; + use crate::KeyPair; + let keypair = KeyPair::new(); + let mut builder = Biscuit::builder(); + builder.add_fact("user(\"John Doe\")").unwrap(); + builder.add_fact("user(\"Jane Doe\")").unwrap(); + + let biscuit = builder.build(&keypair).unwrap(); + + let mut authorizer = biscuit.authorizer().unwrap(); + let res: Result<(String,), error::Token> = + authorizer.query_exactly_one("data($name) <- user($name)"); + assert_eq!( + res.unwrap_err().to_string(), + "Reached Datalog execution limits" + ); + } + #[test] fn authorizer_with_scopes() { let root = KeyPair::new();