Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions biscuit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)')
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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}
4 changes: 2 additions & 2 deletions docs/basic-use.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down
64 changes: 60 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,6 +39,27 @@ create_exception!(
AuthorizationError,
pyo3::exceptions::PyException
);

#[derive(IntoPyObject)]
struct AuthorizationErrorData {
matched_policy: Option<MatchedPolicyData>,
checks: Vec<FailedCheckData>,
}

#[derive(IntoPyObject)]
pub struct MatchedPolicyData {
policy_id: usize,
code: String,
}

#[derive(IntoPyObject)]
struct FailedCheckData {
authorizer_check: bool,
block_id: Option<u32>,
check_id: u32,
code: String,
}

create_exception!(
biscuit_auth,
BiscuitBuildError,
Expand Down Expand Up @@ -785,10 +807,44 @@ impl PyAuthorizer {
///
/// :return: the index of the matched allow rule
/// :rtype: int
pub fn authorize(&mut self) -> PyResult<usize> {
self.0
.authorize()
.map_err(|error| AuthorizationError::new_err(error.to_string()))
pub fn authorize(&mut self) -> PyResult<MatchedPolicyData> {
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
Expand Down