use super::utils::cedarling_util::get_cedarling_with_callback;
use super::utils::*;
use crate::authz::BatchValidationError;
use crate::authz::request::{
BatchAuthorizeMultiIssuerRequest, BatchItem, EntityData, TokenInput,
};
use crate::{BatchItemError, Cedarling, MultiIssuerAuthorizeResult};
use serde_json::json;
fn expect_ok(
r: &Result<MultiIssuerAuthorizeResult, BatchItemError>,
idx: usize,
) -> &MultiIssuerAuthorizeResult {
r.as_ref()
.unwrap_or_else(|e| panic!("item {idx} expected Ok, got Err: {e:?}"))
}
async fn get_cedarling_for_multi_issuer_tests() -> Cedarling {
static POLICY_STORE_RAW_YAML: &str =
include_str!("../../../test_files/policy-store-multi-issuer-basic.yaml");
get_cedarling_with_callback(
PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string()),
|_config| {},
)
.await
}
fn dolphin_userinfo_token() -> TokenInput {
let payload = generate_token_using_claims(json!({
"iss": "https://idp.dolphin.sea",
"sub": "dolphin_user_123",
"jti": "dolphin_user_123",
"client_id": "dolphin_client_123",
"aud": "dolphin_audience",
"exp": 2_000_000_000,
"iat": 1_516_239_022,
"role": ["admin", "user"],
}));
TokenInput::new("Dolphin::Userinfo_token".to_string(), payload)
}
fn approved_dolphin_foods_resource(id: &str) -> EntityData {
EntityData::from_json(
&json!({
"cedar_entity_mapping": {
"entity_type": "Acme::Resource",
"id": id,
},
"name": "Approved Dolphin Foods",
})
.to_string(),
)
.expect("resource should build")
}
fn allowing_item() -> BatchItem {
BatchItem {
resource: approved_dolphin_foods_resource("ApprovedDolphinFoods"),
action: "Acme::Action::\"CheckRoleFoodApprover\"".to_string(),
context: json!({}),
}
}
fn denying_item(id: &str) -> BatchItem {
BatchItem {
resource: approved_dolphin_foods_resource(id),
action: "Acme::Action::\"CheckRoleFoodApprover\"".to_string(),
context: json!({}),
}
}
#[tokio::test]
async fn batch_multi_issuer_single_item_allow() {
let cedarling = get_cedarling_for_multi_issuer_tests().await;
let request = BatchAuthorizeMultiIssuerRequest::new(
vec![dolphin_userinfo_token()],
vec![allowing_item()],
);
let response = cedarling
.authorize_multi_issuer_batch(request)
.await
.expect("batch should be processed");
assert_eq!(response.results.len(), 1);
assert!(
expect_ok(&response.results[0], 0).decision,
"single item should allow via role mapping"
);
assert!(!response.batch_id.to_string().is_empty());
}
#[tokio::test]
async fn batch_multi_issuer_alternating_decisions_preserve_order() {
let cedarling = get_cedarling_for_multi_issuer_tests().await;
let items: Vec<BatchItem> = (0..10)
.map(|i| {
if i % 2 == 0 {
allowing_item()
} else {
denying_item(&format!("wrong-resource-{i}"))
}
})
.collect();
let request = BatchAuthorizeMultiIssuerRequest::new(vec![dolphin_userinfo_token()], items);
let response = cedarling
.authorize_multi_issuer_batch(request)
.await
.expect("batch should be processed");
assert_eq!(response.results.len(), 10);
for (i, r) in response.results.iter().enumerate() {
let expected_allow = i % 2 == 0;
assert_eq!(
expect_ok(r, i).decision,
expected_allow,
"item {i} decision must match its input position"
);
}
}
#[tokio::test]
async fn batch_multi_issuer_n25_all_allow() {
let cedarling = get_cedarling_for_multi_issuer_tests().await;
let items: Vec<BatchItem> = (0..25).map(|_| allowing_item()).collect();
let request = BatchAuthorizeMultiIssuerRequest::new(vec![dolphin_userinfo_token()], items);
let response = cedarling
.authorize_multi_issuer_batch(request)
.await
.expect("batch should be processed");
assert_eq!(response.results.len(), 25);
for (i, r) in response.results.iter().enumerate() {
assert!(expect_ok(r, i).decision, "item {i} should allow");
}
}
#[tokio::test]
async fn batch_multi_issuer_empty_tokens_rejected() {
let cedarling = get_cedarling_for_multi_issuer_tests().await;
let request = BatchAuthorizeMultiIssuerRequest::new(vec![], vec![allowing_item()]);
let err = cedarling
.authorize_multi_issuer_batch(request)
.await
.expect_err("empty tokens should be rejected");
assert!(
matches!(
err,
crate::AuthorizeError::BatchValidation(BatchValidationError::EmptyTokens)
),
"expected EmptyTokens, got: {err:?}"
);
}
#[tokio::test]
async fn batch_multi_issuer_empty_items_rejected() {
let cedarling = get_cedarling_for_multi_issuer_tests().await;
let request = BatchAuthorizeMultiIssuerRequest::new(vec![dolphin_userinfo_token()], vec![]);
let err = cedarling
.authorize_multi_issuer_batch(request)
.await
.expect_err("empty items should be rejected");
assert!(
matches!(
err,
crate::AuthorizeError::BatchValidation(BatchValidationError::EmptyItems)
),
"expected EmptyItems, got: {err:?}"
);
}
#[tokio::test]
async fn batch_multi_issuer_non_object_context_rejected() {
let cedarling = get_cedarling_for_multi_issuer_tests().await;
let bad = BatchItem {
resource: approved_dolphin_foods_resource("bad"),
action: "Acme::Action::\"CheckRoleFoodApprover\"".to_string(),
context: json!("string-not-object"),
};
let request = BatchAuthorizeMultiIssuerRequest::new(
vec![dolphin_userinfo_token()],
vec![allowing_item(), bad],
);
let err = cedarling
.authorize_multi_issuer_batch(request)
.await
.expect_err("non-object context should be rejected");
assert!(
matches!(
err,
crate::AuthorizeError::BatchValidation(
BatchValidationError::InvalidItemContext { index: 1 }
)
),
"expected InvalidItemContext {{index: 1}}, got: {err:?}"
);
}
#[tokio::test]
async fn batch_multi_issuer_bad_action_surfaces_error_only_at_that_item() {
let cedarling = get_cedarling_for_multi_issuer_tests().await;
let request = BatchAuthorizeMultiIssuerRequest::new(
vec![dolphin_userinfo_token()],
vec![
allowing_item(),
BatchItem {
resource: approved_dolphin_foods_resource("bad"),
action: "this is not a valid uid".to_string(),
context: json!({}),
},
allowing_item(),
],
);
let response = cedarling
.authorize_multi_issuer_batch(request)
.await
.expect("batch succeeds even when one item has a bad action");
assert_eq!(response.results.len(), 3);
assert!(expect_ok(&response.results[0], 0).decision, "item 0 allowed");
match &response.results[1] {
Err(BatchItemError::ActionParse { item_index, .. }) => {
assert_eq!(*item_index, 1, "item_index in error must match position");
},
other => panic!("item 1 must surface ActionParse error, got: {other:?}"),
}
assert!(expect_ok(&response.results[2], 2).decision, "item 2 allowed");
}