diff --git a/biscuit_test.py b/biscuit_test.py index 8d49186..e3f6a09 100644 --- a/biscuit_test.py +++ b/biscuit_test.py @@ -4,7 +4,7 @@ import pytest -from biscuit_auth import Algorithm, KeyPair, Authorizer, AuthorizerBuilder, Biscuit, BiscuitBuilder, BlockBuilder, Check, Fact, KeyPair, Policy, PrivateKey, PublicKey, Rule, UnverifiedBiscuit +from biscuit_auth import Algorithm, KeyPair, Authorizer, AuthorizerBuilder, Biscuit, BiscuitBuilder, BlockBuilder, Check, Fact, KeyPair, Policy, PrivateKey, PublicKey, Rule, UnverifiedBiscuit, AuthorizationError def test_fact(): fact = Fact('fact(1, true, "", "Test", hex:aabbcc, 2023-04-29T01:00:00Z)') @@ -237,6 +237,16 @@ def choose_key(kid): except: pass +def test_authorizer_exception(): + authorizer = AuthorizerBuilder("check if true; reject if true; allow if false; deny if true;").build_unauthenticated() + try: + authorizer.authorize() + assert False + except AuthorizationError as e: + (args,) = e.args + assert args['matched_policy'] == {'code': 'deny if true', 'policy_id': 1} + assert args['checks'] == [{'authorizer_check': True, 'block_id': None, 'check_id': 1, 'code': 'reject if true'}] + def test_complete_lifecycle(): private_key = PrivateKey("ed25519-private/473b5189232f3f597b5c2f3f9b0d5e28b1ee4e7cce67ec6b7fbf5984157a6b97") root = KeyPair.from_private_key(private_key) @@ -257,7 +267,7 @@ def test_complete_lifecycle(): policy = authorizer.authorize() - assert policy == 0 + assert policy == {'code': 'allow if user("1234")', 'policy_id': 0} rule = Rule("u($id) <- user($id), $id == {id}", { 'id': "1234"}) facts = authorizer.query(rule) @@ -290,7 +300,7 @@ def test_snapshot(): policy = parsed.authorize() - assert policy == 0 + assert policy == {'code': 'allow if user("1234")', 'policy_id': 0} rule = Rule("u($id) <- user($id), $id == {id}", { 'id': "1234"}) facts = parsed.query(rule) @@ -305,7 +315,7 @@ def test_snapshot(): raw_policy = parsed_from_raw.authorize() - assert raw_policy == 0 + assert raw_policy == {'code': 'allow if user("1234")', 'policy_id': 0} rule = Rule("u($id) <- user($id), $id == {id}", { 'id': "1234"}) raw_facts = parsed_from_raw.query(rule) @@ -461,5 +471,4 @@ def test(left, right): 'other': lambda x : x == 2, }) policy = authorizer.build_unauthenticated().authorize() - assert policy == 0 - + assert policy == {'code': 'allow if 1.extern::test(1)', 'policy_id': 0} diff --git a/docs/basic-use.rst b/docs/basic-use.rst index abeda7f..130a301 100644 --- a/docs/basic-use.rst +++ b/docs/basic-use.rst @@ -74,7 +74,7 @@ Parse and authorize a biscuit token >>> token = Biscuit.from_base64("En0KEwoEMTIzNBgDIgkKBwgKEgMYgAgSJAgAEiCp8D9laR_CXmFmiUlo6zi8L63iapXDxX1evELp4HVaBRpAx3Mkwu2f2AcNq48IZwu-pxACq1stL76DSMGEugmiduuTVwMqLmgKZ4VFgzeydCrYY_Id3MkxgTgjXzEHUH4DDSIiCiB55I7ykL9wQXHRDqUnSgZwCdYNdO7c8LZEj0VH5sy3-Q==", public_key) >>> authorizer = AuthorizerBuilder( """ time({now}); allow if user($u); """, { 'now': datetime.now(tz = timezone.utc)} ).build(token) >>> authorizer.authorize() -0 +{'policy_id': 0, 'code': 'allow if user($u)'} In order to help with key rotation, biscuit tokens can optionally carry a root key identifier, helping the verifying party choose between several valid public keys. @@ -88,7 +88,7 @@ In order to help with key rotation, biscuit tokens can optionally carry a root k >>> token = Biscuit.from_base64("CAESfQoTCgQxMjM0GAMiCQoHCAoSAxiACBIkCAASII5WVsvM52T91C12wnzButmyzmtGSX_rbM6hCSIJihX2GkDwAcVxTnY8aeMLm-i2R_VzTfIMQZya49ogXO2h2Fg2TJsDcG3udIki9il5PA05lKUwrfPNroS7Qg5e04AyLLcHIiIKII5rh75jrCrgE6Rzw6GVYczMn1IOo287uO4Ef5wp7obY", public_key_fn) >>> authorizer = AuthorizerBuilder( """ time({now}); allow if user($u); """, { 'now': datetime.now(tz = timezone.utc)} ).build(token) >>> authorizer.authorize() -0 +{'policy_id': 0, 'code': 'allow if user($u)'} It is possible to parse a biscuit token without verifying its signatures,for instance to inspect its contents, extract revocation ids or append a block. diff --git a/src/lib.rs b/src/lib.rs index cf049dd..cb33d3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ #![allow(clippy::useless_conversion)] use ::biscuit_auth::builder::MapKey; use ::biscuit_auth::datalog::ExternFunc; +use ::biscuit_auth::error::MatchedPolicy; use ::biscuit_auth::AuthorizerBuilder; use ::biscuit_auth::RootKeyProvider; use ::biscuit_auth::UnverifiedBiscuit; @@ -38,6 +39,27 @@ create_exception!( AuthorizationError, pyo3::exceptions::PyException ); + +#[derive(IntoPyObject)] +struct AuthorizationErrorData { + matched_policy: Option, + checks: Vec, +} + +#[derive(IntoPyObject)] +pub struct MatchedPolicyData { + policy_id: usize, + code: String, +} + +#[derive(IntoPyObject)] +struct FailedCheckData { + authorizer_check: bool, + block_id: Option, + check_id: u32, + code: String, +} + create_exception!( biscuit_auth, BiscuitBuildError, @@ -785,10 +807,44 @@ impl PyAuthorizer { /// /// :return: the index of the matched allow rule /// :rtype: int - pub fn authorize(&mut self) -> PyResult { - self.0 - .authorize() - .map_err(|error| AuthorizationError::new_err(error.to_string())) + pub fn authorize(&mut self) -> PyResult { + let all_policies = self.0.dump().3; + let policy_id = self.0.authorize().map_err(|error| match error { + error::Token::FailedLogic(error::Logic::Unauthorized { + policy: MatchedPolicy::Deny(pid), + checks, + }) => AuthorizationError::new_err(AuthorizationErrorData { + matched_policy: Some(MatchedPolicyData { + policy_id: pid, + code: all_policies.get(pid).unwrap().to_string(), + }), + checks: checks + .into_iter() + .map(|c| match c { + error::FailedCheck::Block(failed_block_check) => FailedCheckData { + authorizer_check: false, + block_id: Some(failed_block_check.block_id), + check_id: failed_block_check.check_id, + code: failed_block_check.rule, + }, + error::FailedCheck::Authorizer(failed_authorizer_check) => { + FailedCheckData { + authorizer_check: true, + block_id: None, + check_id: failed_authorizer_check.check_id, + code: failed_authorizer_check.rule, + } + } + }) + .collect(), + }), + _ => AuthorizationError::new_err(error.to_string()), + })?; + + Ok(MatchedPolicyData { + policy_id, + code: all_policies.get(policy_id).unwrap().to_string(), + }) } /// Query the authorizer by returning all the `Fact`s generated by the provided `Rule`. The generated facts won't be