use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid7::Uuid;
use super::errors::{BatchValidationError, MultiIssuerValidationError, TokenInputError};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RequestUnsigned {
pub principal: Option<EntityData>,
pub action: String,
pub resource: EntityData,
pub context: Value,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct EntityData {
#[serde(rename = "cedar_entity_mapping")]
pub cedar_mapping: CedarEntityMapping,
#[serde(flatten)]
pub attributes: HashMap<String, Value>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct CedarEntityMapping {
#[serde(rename = "entity_type")]
pub entity_type: String,
pub id: String,
}
impl EntityData {
pub fn from_json(entity_data: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str::<Self>(entity_data)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TokenInput {
pub mapping: String,
pub payload: String,
}
impl TokenInput {
#[must_use]
pub fn new(mapping: String, payload: String) -> Self {
Self { mapping, payload }
}
pub fn validate(&self) -> Result<(), TokenInputError> {
if self.mapping.trim().is_empty() {
return Err(TokenInputError::EmptyMapping);
}
if self.payload.trim().is_empty() {
return Err(TokenInputError::EmptyPayload);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuthorizeMultiIssuerRequest {
pub tokens: Vec<TokenInput>,
pub resource: EntityData,
pub action: String,
pub context: Option<Value>,
}
impl AuthorizeMultiIssuerRequest {
#[must_use]
pub fn new(tokens: Vec<TokenInput>, resource: EntityData, action: String) -> Self {
Self {
tokens,
resource,
action,
context: None,
}
}
#[must_use]
pub fn new_with_fields(
tokens: Vec<TokenInput>,
resource: EntityData,
action: String,
context: Option<Value>,
) -> Self {
Self {
tokens,
resource,
action,
context,
}
}
pub fn validate(&self) -> Result<(), MultiIssuerValidationError> {
if self.tokens.is_empty() {
return Err(MultiIssuerValidationError::EmptyTokenArray);
}
if let Some(ref context) = self.context
&& !context.is_object()
{
return Err(MultiIssuerValidationError::InvalidContextJson);
}
Ok(())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct BatchItem {
pub resource: EntityData,
pub action: String,
#[serde(default = "empty_object")]
pub context: Value,
}
fn empty_object() -> Value {
Value::Object(serde_json::Map::new())
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BatchAuthorizeUnsignedRequest {
pub principal: Option<EntityData>,
pub items: Vec<BatchItem>,
}
impl BatchAuthorizeUnsignedRequest {
#[must_use]
pub fn new(principal: Option<EntityData>, items: Vec<BatchItem>) -> Self {
Self { principal, items }
}
pub fn validate(&self) -> Result<(), BatchValidationError> {
if self.items.is_empty() {
return Err(BatchValidationError::EmptyItems);
}
for (index, item) in self.items.iter().enumerate() {
if !item.context.is_object() {
return Err(BatchValidationError::InvalidItemContext { index });
}
}
Ok(())
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BatchAuthorizeMultiIssuerRequest {
pub tokens: Vec<TokenInput>,
pub items: Vec<BatchItem>,
}
impl BatchAuthorizeMultiIssuerRequest {
#[must_use]
pub fn new(tokens: Vec<TokenInput>, items: Vec<BatchItem>) -> Self {
Self { tokens, items }
}
pub fn validate(&self) -> Result<(), BatchValidationError> {
if self.tokens.is_empty() {
return Err(BatchValidationError::EmptyTokens);
}
if self.items.is_empty() {
return Err(BatchValidationError::EmptyItems);
}
for (index, item) in self.items.iter().enumerate() {
if !item.context.is_object() {
return Err(BatchValidationError::InvalidItemContext { index });
}
}
Ok(())
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BatchAuthorizeResponse<R> {
pub batch_id: Uuid,
pub results: Vec<R>,
}
impl<R> BatchAuthorizeResponse<R> {
#[must_use]
pub fn new(batch_id: Uuid, results: Vec<R>) -> Self {
Self { batch_id, results }
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use test_utils::token_claims::generate_token_using_claims;
fn create_test_token(mapping: &str, issuer: &str, sub: &str) -> TokenInput {
let claims = json!({
"sub": sub,
"iat": 1_516_239_022,
"iss": issuer
});
let token_string = generate_token_using_claims(&claims);
TokenInput::new(mapping.to_string(), token_string)
}
#[test]
fn test_token_input_creation() {
let token = create_test_token("Jans::Access_Token", "https://example.com", "1234567890");
assert_eq!(token.mapping, "Jans::Access_Token");
assert!(token.payload.contains('.')); }
#[test]
fn test_token_input_validate_success() {
let token = create_test_token("Jans::Access_Token", "https://example.com", "1234567890");
let result = token.validate();
assert!(result.is_ok());
}
#[test]
fn test_token_input_validate_empty_mapping() {
let token = TokenInput::new(String::new(), "valid.jwt.token".to_string());
let result = token.validate();
assert!(matches!(result, Err(TokenInputError::EmptyMapping)));
}
#[test]
fn test_token_input_validate_empty_payload() {
let token = TokenInput::new("Jans::Access_Token".to_string(), String::new());
let result = token.validate();
assert!(matches!(result, Err(TokenInputError::EmptyPayload)));
}
#[test]
fn test_authorize_multi_issuer_request_creation() {
let tokens = vec![
create_test_token("Jans::Access_Token", "https://example.com", "1234567890"),
create_test_token("Jans::Id_Token", "https://example.com", "1234567890"),
];
let resource = EntityData {
cedar_mapping: CedarEntityMapping {
entity_type: "Document".to_string(),
id: "doc123".to_string(),
},
attributes: HashMap::new(),
};
let request =
AuthorizeMultiIssuerRequest::new(tokens.clone(), resource.clone(), "Read".to_string());
assert_eq!(request.tokens.len(), 2);
assert_eq!(request.resource, resource);
assert_eq!(request.action, "Read");
assert!(request.context.is_none());
}
#[test]
fn test_authorize_multi_issuer_request_with_fields() {
let tokens = vec![create_test_token(
"Jans::Access_Token",
"https://example.com",
"1234567890",
)];
let resource = EntityData {
cedar_mapping: CedarEntityMapping {
entity_type: "Document".to_string(),
id: "doc123".to_string(),
},
attributes: HashMap::new(),
};
let action = "Read".to_string();
let context = Some(json!({"location": "miami"}));
let request = AuthorizeMultiIssuerRequest::new_with_fields(
tokens,
resource.clone(),
action.clone(),
context.clone(),
);
assert_eq!(request.tokens.len(), 1);
assert_eq!(request.resource, resource);
assert_eq!(request.action, action);
assert_eq!(request.context, context);
}
#[test]
fn test_authorize_multi_issuer_request_validation_success() {
let tokens = vec![
create_test_token("Jans::Access_Token", "https://example.com", "1234567890"),
create_test_token("Jans::Id_Token", "https://example.com", "1234567890"),
];
let resource = EntityData {
cedar_mapping: CedarEntityMapping {
entity_type: "Document".to_string(),
id: "doc123".to_string(),
},
attributes: HashMap::new(),
};
let request = AuthorizeMultiIssuerRequest::new(tokens, resource, "Read".to_string());
assert!(request.validate().is_ok());
}
#[test]
fn test_authorize_multi_issuer_request_validation_empty_tokens() {
let resource = EntityData {
cedar_mapping: CedarEntityMapping {
entity_type: "Document".to_string(),
id: "doc123".to_string(),
},
attributes: HashMap::new(),
};
let request = AuthorizeMultiIssuerRequest::new(vec![], resource, "Read".to_string());
let result = request.validate();
assert!(matches!(
result,
Err(MultiIssuerValidationError::EmptyTokenArray)
));
}
#[test]
fn test_authorize_multi_issuer_request_validation_invalid_token() {
let tokens = vec![TokenInput::new(
"valid-mapping".to_string(), "some-payload".to_string(),
)];
let resource = EntityData {
cedar_mapping: CedarEntityMapping {
entity_type: "Document".to_string(),
id: "doc123".to_string(),
},
attributes: HashMap::new(),
};
let request = AuthorizeMultiIssuerRequest::new(tokens, resource, "Read".to_string());
let result = request.validate();
assert!(result.is_ok());
}
#[test]
fn test_authorize_multi_issuer_request_validation_invalid_json_fields() {
let tokens = vec![create_test_token(
"Jans::Access_Token",
"https://example.com",
"1234567890",
)];
let resource = EntityData {
cedar_mapping: CedarEntityMapping {
entity_type: "Document".to_string(),
id: "doc123".to_string(),
},
attributes: HashMap::new(),
};
let request = AuthorizeMultiIssuerRequest::new_with_fields(
tokens,
resource, "Read".to_string(), Some(json!(123)), );
let result = request.validate();
assert!(matches!(
result,
Err(MultiIssuerValidationError::InvalidContextJson)
));
}
#[test]
fn test_serialization_deserialization() {
let tokens = vec![create_test_token(
"Jans::Access_Token",
"https://example.com",
"1234567890",
)];
let resource = EntityData {
cedar_mapping: CedarEntityMapping {
entity_type: "Document".to_string(),
id: "doc123".to_string(),
},
attributes: HashMap::new(),
};
let request = AuthorizeMultiIssuerRequest::new_with_fields(
tokens,
resource,
"Read".to_string(),
Some(json!({"location": "miami"})),
);
let json = serde_json::to_string(&request).expect("Should serialize");
let deserialized: AuthorizeMultiIssuerRequest =
serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(request, deserialized);
}
fn make_resource(id: &str) -> EntityData {
EntityData {
cedar_mapping: CedarEntityMapping {
entity_type: "Document".to_string(),
id: id.to_string(),
},
attributes: HashMap::new(),
}
}
fn make_item(id: &str) -> BatchItem {
BatchItem {
resource: make_resource(id),
action: "Read".to_string(),
context: json!({}),
}
}
#[test]
fn batch_unsigned_validates_valid_request() {
let req = BatchAuthorizeUnsignedRequest::new(None, vec![make_item("a"), make_item("b")]);
req.validate()
.expect("well-formed unsigned request should validate");
}
#[test]
fn batch_unsigned_rejects_empty_items() {
let req = BatchAuthorizeUnsignedRequest::new(None, vec![]);
assert_eq!(
req.validate(),
Err(BatchValidationError::EmptyItems),
"validation should reject empty items list"
);
}
#[test]
fn batch_unsigned_rejects_non_object_context() {
let bad_item = BatchItem {
resource: make_resource("bad"),
action: "Read".to_string(),
context: json!(42),
};
let req = BatchAuthorizeUnsignedRequest::new(None, vec![make_item("a"), bad_item]);
assert_eq!(
req.validate(),
Err(BatchValidationError::InvalidItemContext { index: 1 }),
"validation should reject non-object context at index 1"
);
}
#[test]
fn batch_multi_issuer_validates_valid_request() {
let tokens = vec![create_test_token(
"Jans::Access_Token",
"https://example.com",
"sub",
)];
let req = BatchAuthorizeMultiIssuerRequest::new(tokens, vec![make_item("a")]);
req.validate()
.expect("well-formed multi-issuer request should validate");
}
#[test]
fn batch_multi_issuer_rejects_empty_tokens() {
let req = BatchAuthorizeMultiIssuerRequest::new(vec![], vec![make_item("a")]);
assert_eq!(
req.validate(),
Err(BatchValidationError::EmptyTokens),
"validation should reject empty tokens list"
);
}
#[test]
fn batch_multi_issuer_rejects_empty_items() {
let tokens = vec![create_test_token(
"Jans::Access_Token",
"https://example.com",
"sub",
)];
let req = BatchAuthorizeMultiIssuerRequest::new(tokens, vec![]);
assert_eq!(
req.validate(),
Err(BatchValidationError::EmptyItems),
"validation should reject empty items list"
);
}
#[test]
fn batch_multi_issuer_rejects_non_object_context() {
let tokens = vec![create_test_token(
"Jans::Access_Token",
"https://example.com",
"sub",
)];
let bad_item = BatchItem {
resource: make_resource("bad"),
action: "Read".to_string(),
context: json!("string-not-object"),
};
let req = BatchAuthorizeMultiIssuerRequest::new(tokens, vec![bad_item]);
assert_eq!(
req.validate(),
Err(BatchValidationError::InvalidItemContext { index: 0 }),
"validation should reject non-object context at index 0"
);
}
#[test]
fn batch_unsigned_round_trips_json() {
let req = BatchAuthorizeUnsignedRequest::new(
Some(make_resource("me")),
vec![
BatchItem {
resource: make_resource("a"),
action: "Read".to_string(),
context: json!({"ip": "10.0.0.1"}),
},
BatchItem {
resource: make_resource("b"),
action: "Write".to_string(),
context: json!({}),
},
],
);
let s = serde_json::to_string(&req).expect("serialize");
let round: BatchAuthorizeUnsignedRequest = serde_json::from_str(&s).expect("deserialize");
assert_eq!(
round.items.len(),
2,
"round-trip should preserve items length"
);
assert_eq!(
round.items[0].action, "Read",
"round-trip should preserve item 0 action"
);
assert_eq!(
round.items[1].action, "Write",
"round-trip should preserve item 1 action"
);
assert!(
round.principal.is_some(),
"round-trip should preserve principal presence"
);
}
#[test]
fn batch_multi_issuer_round_trips_json() {
let tokens = vec![
create_test_token("Jans::Access_Token", "https://example.com", "sub-a"),
create_test_token("Jans::Id_Token", "https://example.com", "sub-b"),
];
let req = BatchAuthorizeMultiIssuerRequest::new(
tokens,
vec![make_item("a"), make_item("b"), make_item("c")],
);
let s = serde_json::to_string(&req).expect("serialize");
let round: BatchAuthorizeMultiIssuerRequest =
serde_json::from_str(&s).expect("deserialize");
assert_eq!(
round.tokens.len(),
2,
"round-trip should preserve tokens length"
);
assert_eq!(
round.items.len(),
3,
"round-trip should preserve items length"
);
}
#[test]
fn batch_response_round_trips_json() {
use crate::log::gen_uuid7;
let response: BatchAuthorizeResponse<String> =
BatchAuthorizeResponse::new(gen_uuid7(), vec!["allow".to_string(), "deny".to_string()]);
let s = serde_json::to_string(&response).expect("serialize");
let round: BatchAuthorizeResponse<String> = serde_json::from_str(&s).expect("deserialize");
assert_eq!(
round.batch_id, response.batch_id,
"round-trip should preserve batch_id"
);
assert_eq!(
round.results, response.results,
"round-trip should preserve results list"
);
}
}