use super::attribute::AttributeMap;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const AUTHZEN_PROFILE_V1: &str = "axonflow-authzen-profile-2026-08-29";
pub const AUTHZEN_CONTRACT_SCHEMA_VERSION: &str = "2026-08-29";
pub const AUTHZEN_PATH: &str = "/api/v1/access/evaluation";
pub const AUTHZEN_PROFILE_HEADER: &str = "X-Axonflow-AuthZEN-Profile";
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum AuthZenErrorCode {
MalformedEnvelope,
IncompleteEvaluation,
UnsupportedSubject,
UnsupportedAction,
UnsupportedResource,
UnevaluableAttribute,
MissingEvaluableContent,
EvaluationUnavailable,
Unknown(String),
}
impl AuthZenErrorCode {
pub const KNOWN_WIRE_VALUES: &'static [&'static str] = &[
"malformed_envelope",
"incomplete_evaluation",
"unsupported_subject",
"unsupported_action",
"unsupported_resource",
"unevaluable_attribute",
"missing_evaluable_content",
"evaluation_unavailable",
];
pub fn as_str(&self) -> &str {
match self {
Self::MalformedEnvelope => "malformed_envelope",
Self::IncompleteEvaluation => "incomplete_evaluation",
Self::UnsupportedSubject => "unsupported_subject",
Self::UnsupportedAction => "unsupported_action",
Self::UnsupportedResource => "unsupported_resource",
Self::UnevaluableAttribute => "unevaluable_attribute",
Self::MissingEvaluableContent => "missing_evaluable_content",
Self::EvaluationUnavailable => "evaluation_unavailable",
Self::Unknown(v) => v.as_str(),
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown(_))
}
}
impl From<String> for AuthZenErrorCode {
fn from(v: String) -> Self {
match v.as_str() {
"malformed_envelope" => Self::MalformedEnvelope,
"incomplete_evaluation" => Self::IncompleteEvaluation,
"unsupported_subject" => Self::UnsupportedSubject,
"unsupported_action" => Self::UnsupportedAction,
"unsupported_resource" => Self::UnsupportedResource,
"unevaluable_attribute" => Self::UnevaluableAttribute,
"missing_evaluable_content" => Self::MissingEvaluableContent,
"evaluation_unavailable" => Self::EvaluationUnavailable,
_ => Self::Unknown(v),
}
}
}
impl From<AuthZenErrorCode> for String {
fn from(v: AuthZenErrorCode) -> Self {
v.as_str().to_string()
}
}
impl std::fmt::Display for AuthZenErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum AuthZenCategory {
Allowed,
NotPermitted,
ApprovalRequired,
TemporarilyUnavailable,
InvalidRequest,
Unknown(String),
}
impl AuthZenCategory {
pub const KNOWN_WIRE_VALUES: &'static [&'static str] = &[
"allowed",
"not_permitted",
"approval_required",
"temporarily_unavailable",
"invalid_request",
];
pub fn as_str(&self) -> &str {
match self {
Self::Allowed => "allowed",
Self::NotPermitted => "not_permitted",
Self::ApprovalRequired => "approval_required",
Self::TemporarilyUnavailable => "temporarily_unavailable",
Self::InvalidRequest => "invalid_request",
Self::Unknown(v) => v.as_str(),
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown(_))
}
}
impl From<String> for AuthZenCategory {
fn from(v: String) -> Self {
match v.as_str() {
"allowed" => Self::Allowed,
"not_permitted" => Self::NotPermitted,
"approval_required" => Self::ApprovalRequired,
"temporarily_unavailable" => Self::TemporarilyUnavailable,
"invalid_request" => Self::InvalidRequest,
_ => Self::Unknown(v),
}
}
}
impl From<AuthZenCategory> for String {
fn from(v: AuthZenCategory) -> Self {
v.as_str().to_string()
}
}
impl std::fmt::Display for AuthZenCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum AuthZenIdentifierKind {
Organization,
Principal,
Group,
Resource,
Action,
Tool,
Client,
Session,
Unknown(String),
}
impl AuthZenIdentifierKind {
pub const KNOWN_WIRE_VALUES: &'static [&'static str] = &[
"organization",
"principal",
"group",
"resource",
"action",
"tool",
"client",
"session",
];
pub fn as_str(&self) -> &str {
match self {
Self::Organization => "organization",
Self::Principal => "principal",
Self::Group => "group",
Self::Resource => "resource",
Self::Action => "action",
Self::Tool => "tool",
Self::Client => "client",
Self::Session => "session",
Self::Unknown(v) => v.as_str(),
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown(_))
}
}
impl From<String> for AuthZenIdentifierKind {
fn from(v: String) -> Self {
match v.as_str() {
"organization" => Self::Organization,
"principal" => Self::Principal,
"group" => Self::Group,
"resource" => Self::Resource,
"action" => Self::Action,
"tool" => Self::Tool,
"client" => Self::Client,
"session" => Self::Session,
_ => Self::Unknown(v),
}
}
}
impl From<AuthZenIdentifierKind> for String {
fn from(v: AuthZenIdentifierKind) -> Self {
v.as_str().to_string()
}
}
impl std::fmt::Display for AuthZenIdentifierKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum AuthZenObligationType {
ApprovalChallenge,
FieldRemove,
FieldRedact,
FieldHash,
FieldMask,
FieldAnnotate,
FieldTokenize,
SchemaTransform,
ResponseFilter,
RouteRestriction,
StepUpAuthentication,
QuotaReservation,
ImmutableAudit,
Notification,
Unknown(String),
}
impl AuthZenObligationType {
pub const KNOWN_WIRE_VALUES: &'static [&'static str] = &[
"approval_challenge",
"field_remove",
"field_redact",
"field_hash",
"field_mask",
"field_annotate",
"field_tokenize",
"schema_transform",
"response_filter",
"route_restriction",
"step_up_authentication",
"quota_reservation",
"immutable_audit",
"notification",
];
pub fn as_str(&self) -> &str {
match self {
Self::ApprovalChallenge => "approval_challenge",
Self::FieldRemove => "field_remove",
Self::FieldRedact => "field_redact",
Self::FieldHash => "field_hash",
Self::FieldMask => "field_mask",
Self::FieldAnnotate => "field_annotate",
Self::FieldTokenize => "field_tokenize",
Self::SchemaTransform => "schema_transform",
Self::ResponseFilter => "response_filter",
Self::RouteRestriction => "route_restriction",
Self::StepUpAuthentication => "step_up_authentication",
Self::QuotaReservation => "quota_reservation",
Self::ImmutableAudit => "immutable_audit",
Self::Notification => "notification",
Self::Unknown(v) => v.as_str(),
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown(_))
}
}
impl From<String> for AuthZenObligationType {
fn from(v: String) -> Self {
match v.as_str() {
"approval_challenge" => Self::ApprovalChallenge,
"field_remove" => Self::FieldRemove,
"field_redact" => Self::FieldRedact,
"field_hash" => Self::FieldHash,
"field_mask" => Self::FieldMask,
"field_annotate" => Self::FieldAnnotate,
"field_tokenize" => Self::FieldTokenize,
"schema_transform" => Self::SchemaTransform,
"response_filter" => Self::ResponseFilter,
"route_restriction" => Self::RouteRestriction,
"step_up_authentication" => Self::StepUpAuthentication,
"quota_reservation" => Self::QuotaReservation,
"immutable_audit" => Self::ImmutableAudit,
"notification" => Self::Notification,
_ => Self::Unknown(v),
}
}
}
impl From<AuthZenObligationType> for String {
fn from(v: AuthZenObligationType) -> Self {
v.as_str().to_string()
}
}
impl std::fmt::Display for AuthZenObligationType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum AuthZenOperationalState {
Allow,
Deny,
Challenge,
Error,
Unknown(String),
}
impl AuthZenOperationalState {
pub const KNOWN_WIRE_VALUES: &'static [&'static str] = &["ALLOW", "DENY", "CHALLENGE", "ERROR"];
pub fn as_str(&self) -> &str {
match self {
Self::Allow => "ALLOW",
Self::Deny => "DENY",
Self::Challenge => "CHALLENGE",
Self::Error => "ERROR",
Self::Unknown(v) => v.as_str(),
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown(_))
}
}
impl From<String> for AuthZenOperationalState {
fn from(v: String) -> Self {
match v.as_str() {
"ALLOW" => Self::Allow,
"DENY" => Self::Deny,
"CHALLENGE" => Self::Challenge,
"ERROR" => Self::Error,
_ => Self::Unknown(v),
}
}
}
impl From<AuthZenOperationalState> for String {
fn from(v: AuthZenOperationalState) -> Self {
v.as_str().to_string()
}
}
impl std::fmt::Display for AuthZenOperationalState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum AuthZenReasonCode {
Permitted,
ApprovalRequired,
ExplicitConstraint,
NoMatchingPermission,
UnknownConstraint,
UnknownPermission,
UnknownRequirement,
InvalidInput,
EvaluationError,
UnsupportedObligation,
ObligationConflict,
UnknownAction,
UnknownRealm,
SchemaViolation,
DelegationDepthExceeded,
BudgetExhausted,
BindingMismatch,
ApprovalUnsatisfiable,
ApprovalExpired,
AuthoringRejected,
Unknown(String),
}
impl AuthZenReasonCode {
pub const KNOWN_WIRE_VALUES: &'static [&'static str] = &[
"permitted",
"approval_required",
"explicit_constraint",
"no_matching_permission",
"unknown_constraint",
"unknown_permission",
"unknown_requirement",
"invalid_input",
"evaluation_error",
"unsupported_obligation",
"obligation_conflict",
"unknown_action",
"unknown_realm",
"schema_violation",
"delegation_depth_exceeded",
"budget_exhausted",
"binding_mismatch",
"approval_unsatisfiable",
"approval_expired",
"authoring_rejected",
];
pub fn as_str(&self) -> &str {
match self {
Self::Permitted => "permitted",
Self::ApprovalRequired => "approval_required",
Self::ExplicitConstraint => "explicit_constraint",
Self::NoMatchingPermission => "no_matching_permission",
Self::UnknownConstraint => "unknown_constraint",
Self::UnknownPermission => "unknown_permission",
Self::UnknownRequirement => "unknown_requirement",
Self::InvalidInput => "invalid_input",
Self::EvaluationError => "evaluation_error",
Self::UnsupportedObligation => "unsupported_obligation",
Self::ObligationConflict => "obligation_conflict",
Self::UnknownAction => "unknown_action",
Self::UnknownRealm => "unknown_realm",
Self::SchemaViolation => "schema_violation",
Self::DelegationDepthExceeded => "delegation_depth_exceeded",
Self::BudgetExhausted => "budget_exhausted",
Self::BindingMismatch => "binding_mismatch",
Self::ApprovalUnsatisfiable => "approval_unsatisfiable",
Self::ApprovalExpired => "approval_expired",
Self::AuthoringRejected => "authoring_rejected",
Self::Unknown(v) => v.as_str(),
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown(_))
}
}
impl From<String> for AuthZenReasonCode {
fn from(v: String) -> Self {
match v.as_str() {
"permitted" => Self::Permitted,
"approval_required" => Self::ApprovalRequired,
"explicit_constraint" => Self::ExplicitConstraint,
"no_matching_permission" => Self::NoMatchingPermission,
"unknown_constraint" => Self::UnknownConstraint,
"unknown_permission" => Self::UnknownPermission,
"unknown_requirement" => Self::UnknownRequirement,
"invalid_input" => Self::InvalidInput,
"evaluation_error" => Self::EvaluationError,
"unsupported_obligation" => Self::UnsupportedObligation,
"obligation_conflict" => Self::ObligationConflict,
"unknown_action" => Self::UnknownAction,
"unknown_realm" => Self::UnknownRealm,
"schema_violation" => Self::SchemaViolation,
"delegation_depth_exceeded" => Self::DelegationDepthExceeded,
"budget_exhausted" => Self::BudgetExhausted,
"binding_mismatch" => Self::BindingMismatch,
"approval_unsatisfiable" => Self::ApprovalUnsatisfiable,
"approval_expired" => Self::ApprovalExpired,
"authoring_rejected" => Self::AuthoringRejected,
_ => Self::Unknown(v),
}
}
}
impl From<AuthZenReasonCode> for String {
fn from(v: AuthZenReasonCode) -> Self {
v.as_str().to_string()
}
}
impl std::fmt::Display for AuthZenReasonCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenApprovalClause {
pub quorum: i64,
pub eligible: Vec<AuthZenIdentifier>,
}
impl AuthZenApprovalClause {
pub fn new(quorum: i64, eligible: Vec<AuthZenIdentifier>) -> Self {
Self { quorum, eligible }
}
}
impl AuthZenApprovalClause {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if self.eligible.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::MalformedEnvelope,
"eligible needs at least 1 entry",
)
.at(&format!("{at}/eligible")));
}
for (i, v) in self.eligible.iter().enumerate() {
v.validate(&format!("{at}/eligible/{i}"))?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenApprovalRequirement {
pub all_of: Vec<AuthZenApprovalClause>,
pub separation_of_duties: bool,
pub expires_at: String,
}
impl AuthZenApprovalRequirement {
pub fn new(
all_of: Vec<AuthZenApprovalClause>,
separation_of_duties: bool,
expires_at: impl Into<String>,
) -> Self {
Self {
all_of,
separation_of_duties,
expires_at: expires_at.into(),
}
}
}
impl AuthZenApprovalRequirement {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if self.all_of.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::MalformedEnvelope,
"all_of needs at least 1 entry",
)
.at(&format!("{at}/all_of")));
}
for (i, v) in self.all_of.iter().enumerate() {
v.validate(&format!("{at}/all_of/{i}"))?;
}
if self.expires_at.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"expires_at is required",
)
.at(&format!("{at}/expires_at")));
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenAction {
pub name: String,
#[serde(default, skip_serializing_if = "AttributeMap::is_empty")]
pub properties: AttributeMap,
}
impl AuthZenAction {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
properties: AttributeMap::new(),
}
}
}
impl AuthZenAction {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if self.name.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"name is required",
)
.at(&format!("{at}/name")));
}
self.properties.validate(&format!("{at}/properties"))?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenBulk {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<AuthZenSubject>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<AuthZenAction>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource: Option<AuthZenResource>,
#[serde(default, skip_serializing_if = "AttributeMap::is_empty")]
pub context: AttributeMap,
pub evaluations: Vec<AuthZenRequest>,
}
impl AuthZenBulk {
pub fn new(evaluations: Vec<AuthZenRequest>) -> Self {
Self {
subject: None,
action: None,
resource: None,
context: AttributeMap::new(),
evaluations,
}
}
}
impl AuthZenBulk {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if let Some(v) = self.subject.as_ref() {
v.validate(&format!("{at}/subject"))?;
}
if let Some(v) = self.action.as_ref() {
v.validate(&format!("{at}/action"))?;
}
if let Some(v) = self.resource.as_ref() {
v.validate(&format!("{at}/resource"))?;
}
self.context.validate(&format!("{at}/context"))?;
if self.evaluations.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::MalformedEnvelope,
"evaluations needs at least 1 entry",
)
.at(&format!("{at}/evaluations")));
}
for (i, v) in self.evaluations.iter().enumerate() {
v.validate(&format!("{at}/evaluations/{i}"))?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenEnvelope {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evaluation: Option<AuthZenRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evaluations: Option<AuthZenBulk>,
}
impl AuthZenEnvelope {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
let mut present = 0;
if self.evaluation.is_some() {
present += 1;
}
if self.evaluations.is_some() {
present += 1;
}
if present != 1 {
return Err(AuthZenError::new(
AuthZenErrorCode::MalformedEnvelope,
format!("exactly one of evaluation or evaluations must be present, {present} are"),
)
.at(at));
}
if let Some(v) = self.evaluation.as_ref() {
if v.action.is_none() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"evaluation has no action; it has no shared base to inherit one from",
)
.at(&format!("{at}/evaluation/action")));
}
}
if let Some(v) = self.evaluation.as_ref() {
if v.resource.is_none() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"evaluation has no resource; it has no shared base to inherit one from",
)
.at(&format!("{at}/evaluation/resource")));
}
}
if let Some(v) = self.evaluation.as_ref() {
if v.subject.is_none() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"evaluation has no subject; it has no shared base to inherit one from",
)
.at(&format!("{at}/evaluation/subject")));
}
}
if let Some(v) = self.evaluation.as_ref() {
v.validate(&format!("{at}/evaluation"))?;
}
if let Some(v) = self.evaluations.as_ref() {
v.validate(&format!("{at}/evaluations"))?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AuthZenError {
pub code: AuthZenErrorCode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pointer: Option<String>,
pub message: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supported: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
impl AuthZenError {
pub fn new(code: AuthZenErrorCode, message: impl Into<String>) -> Self {
Self {
code,
pointer: None,
message: message.into(),
supported: Vec::new(),
request_id: None,
}
}
}
impl AuthZenError {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if self.code.as_str().is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"code is required",
)
.at(&format!("{at}/code")));
}
if self.message.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"message is required",
)
.at(&format!("{at}/message")));
}
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<AuthZenSubject>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<AuthZenAction>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource: Option<AuthZenResource>,
#[serde(default, skip_serializing_if = "AttributeMap::is_empty")]
pub context: AttributeMap,
}
impl AuthZenRequest {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if let Some(v) = self.subject.as_ref() {
v.validate(&format!("{at}/subject"))?;
}
if let Some(v) = self.action.as_ref() {
v.validate(&format!("{at}/action"))?;
}
if let Some(v) = self.resource.as_ref() {
v.validate(&format!("{at}/resource"))?;
}
self.context.validate(&format!("{at}/context"))?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenResource {
pub r#type: String,
pub id: String,
#[serde(default, skip_serializing_if = "AttributeMap::is_empty")]
pub properties: AttributeMap,
}
impl AuthZenResource {
pub fn new(r#type: impl Into<String>, id: impl Into<String>) -> Self {
Self {
r#type: r#type.into(),
id: id.into(),
properties: AttributeMap::new(),
}
}
}
impl AuthZenResource {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if self.r#type.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"type is required",
)
.at(&format!("{at}/type")));
}
if self.id.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"id is required",
)
.at(&format!("{at}/id")));
}
self.properties.validate(&format!("{at}/properties"))?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenResponse {
pub decision: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<AuthZenResponseContext>,
}
impl AuthZenResponse {
pub fn new(decision: bool) -> Self {
Self {
decision,
context: None,
}
}
}
impl AuthZenResponse {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if let Some(v) = self.context.as_ref() {
v.validate(&format!("{at}/context"))?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenResponseContext {
pub profile: String,
pub state: AuthZenOperationalState,
pub category: AuthZenCategory,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<AuthZenReasonCode>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub obligations: Vec<AuthZenObligation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approval: Option<AuthZenApprovalRequirement>,
pub decision_id: String,
pub schema_version: String,
}
impl AuthZenResponseContext {
pub fn new(
profile: impl Into<String>,
state: AuthZenOperationalState,
category: AuthZenCategory,
decision_id: impl Into<String>,
schema_version: impl Into<String>,
) -> Self {
Self {
profile: profile.into(),
state,
category,
reason: None,
obligations: Vec::new(),
approval: None,
decision_id: decision_id.into(),
schema_version: schema_version.into(),
}
}
}
impl AuthZenResponseContext {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if self.profile.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"profile is required",
)
.at(&format!("{at}/profile")));
}
let v = &self.profile;
if v.as_str() != "axonflow-authzen-profile-2026-08-29" {
return Err(AuthZenError::new(
AuthZenErrorCode::MalformedEnvelope,
format!("profile must be axonflow-authzen-profile-2026-08-29, got {v:?}"),
)
.at(&format!("{at}/profile"))
.supporting(["axonflow-authzen-profile-2026-08-29"]));
}
if self.state.as_str().is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"state is required",
)
.at(&format!("{at}/state")));
}
if self.category.as_str().is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"category is required",
)
.at(&format!("{at}/category")));
}
for (i, v) in self.obligations.iter().enumerate() {
v.validate(&format!("{at}/obligations/{i}"))?;
}
if let Some(v) = self.approval.as_ref() {
v.validate(&format!("{at}/approval"))?;
}
if self.decision_id.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"decision_id is required",
)
.at(&format!("{at}/decision_id")));
}
if self.schema_version.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"schema_version is required",
)
.at(&format!("{at}/schema_version")));
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenSubject {
pub r#type: String,
pub id: String,
#[serde(default, skip_serializing_if = "AttributeMap::is_empty")]
pub properties: AttributeMap,
}
impl AuthZenSubject {
pub fn new(r#type: impl Into<String>, id: impl Into<String>) -> Self {
Self {
r#type: r#type.into(),
id: id.into(),
properties: AttributeMap::new(),
}
}
}
impl AuthZenSubject {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if self.r#type.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"type is required",
)
.at(&format!("{at}/type")));
}
if self.id.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"id is required",
)
.at(&format!("{at}/id")));
}
self.properties.validate(&format!("{at}/properties"))?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenIdentifier {
pub kind: AuthZenIdentifierKind,
pub r#type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub qualifier: Option<String>,
pub local: String,
}
impl AuthZenIdentifier {
pub fn new(
kind: AuthZenIdentifierKind,
r#type: impl Into<String>,
local: impl Into<String>,
) -> Self {
Self {
kind,
r#type: r#type.into(),
qualifier: None,
local: local.into(),
}
}
}
impl AuthZenIdentifier {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if self.kind.as_str().is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"kind is required",
)
.at(&format!("{at}/kind")));
}
if self.r#type.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"type is required",
)
.at(&format!("{at}/type")));
}
if self.local.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"local is required",
)
.at(&format!("{at}/local")));
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AuthZenObligation {
pub r#type: AuthZenObligationType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub params: BTreeMap<String, String>,
pub mandatory: bool,
pub source_policy: String,
pub schema_version: i64,
}
impl AuthZenObligation {
pub fn new(
r#type: AuthZenObligationType,
mandatory: bool,
source_policy: impl Into<String>,
schema_version: i64,
) -> Self {
Self {
r#type,
target: None,
params: BTreeMap::new(),
mandatory,
source_policy: source_policy.into(),
schema_version,
}
}
}
impl AuthZenObligation {
pub fn validate(&self, at: &str) -> Result<(), AuthZenError> {
if self.r#type.as_str().is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"type is required",
)
.at(&format!("{at}/type")));
}
if let Some(v) = self.target.as_ref() {
if v.chars().count() < 1 {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"target needs at least 1 character",
)
.at(&format!("{at}/target")));
}
}
if self.source_policy.is_empty() {
return Err(AuthZenError::new(
AuthZenErrorCode::IncompleteEvaluation,
"source_policy is required",
)
.at(&format!("{at}/source_policy")));
}
Ok(())
}
}