crabka_authz/lib.rs
1//! Shared Kafka-ACL authorization evaluator (broker + gateway).
2//!
3//! Holds the [`Authorizer`] trait + ACL evaluator ([`SimpleAclAuthorizer`] /
4//! [`AllowAllAuthorizer`]) plus an [`AclSource`] abstraction so one evaluator
5//! serves both the broker (a `MetadataImage` snapshot) and the gateway (an
6//! [`AclCache`] over a `Vec<AclEntry>` fetched via `DescribeAcls`). The decision
7//! logic (super-user bypass, deny-wins, operation implication) lives here once
8//! so the two callers can never drift.
9//!
10//! ## Authorizing a request
11//!
12//! ```rust
13//! use crabka_authz::{AllowAllAuthorizer, AuthorizationRequest, AuthorizationResult, Authorizer};
14//! use crabka_metadata::{AclOperation, MetadataImage, ResourceType};
15//! use crabka_security::{AuthMethod, Principal};
16//! use std::net::SocketAddr;
17//! use uuid::Uuid;
18//!
19//! let image = MetadataImage::new(Uuid::nil());
20//! let principal = Principal {
21//! name: "alice".into(),
22//! auth_method: AuthMethod::SaslPlain,
23//! groups: vec![],
24//! };
25//! let host: SocketAddr = "127.0.0.1:9092".parse().unwrap();
26//! let req = AuthorizationRequest {
27//! principal: &principal,
28//! host: &host,
29//! resource_type: ResourceType::Topic,
30//! resource_name: "orders",
31//! operation: AclOperation::Read,
32//! };
33//!
34//! assert_eq!(
35//! AllowAllAuthorizer.authorize(&image, &req),
36//! AuthorizationResult::Allow,
37//! );
38//! ```
39#![forbid(unsafe_code)]
40
41mod allow_all;
42pub mod cache;
43mod simple;
44mod source;
45
46pub use allow_all::AllowAllAuthorizer;
47pub use cache::AclCache;
48pub use simple::SimpleAclAuthorizer;
49pub use source::AclSource;
50
51use std::net::SocketAddr;
52
53use crabka_metadata::{AclOperation, ResourceType};
54use crabka_security::Principal;
55
56/// What `authorize` is being asked: which principal wants to do which
57/// operation on which resource, from which host. References are borrowed
58/// so handler-side construction is allocation-free.
59#[derive(Debug, Clone)]
60pub struct AuthorizationRequest<'a> {
61 pub principal: &'a Principal,
62 pub host: &'a SocketAddr,
63 pub resource_type: ResourceType,
64 pub resource_name: &'a str,
65 pub operation: AclOperation,
66}
67
68/// Binary outcome — Kafka's ACL surface is allow/deny; intermediate
69/// states (e.g. "not yet decided") aren't exposed at the trait boundary.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum AuthorizationResult {
72 Allow,
73 Deny,
74}
75
76/// Pluggable per-broker / per-gateway authorization decision point.
77/// Implementations own whatever state they need to render a decision
78/// (super-user set, HTTP client, decision cache) and the caller holds a
79/// single `Arc<dyn Authorizer>`.
80///
81/// Implementations MUST be `Send + Sync + Debug`: handler code paths
82/// are async and the broker logs configs at startup.
83///
84/// The decision consults a [`AclSource`] — the broker passes its
85/// `MetadataImage`, the gateway passes an [`AclCache`]; ACL-free impls
86/// (`AllowAll`, OPA) ignore it.
87pub trait Authorizer: Send + Sync + std::fmt::Debug {
88 /// Decide whether `req.principal` may perform `req.operation` on
89 /// `(req.resource_type, req.resource_name)` from `req.host`. The
90 /// authorizer is free to consult `source` (for ACL-backed impls)
91 /// or ignore it entirely (`AllowAll`, `Opa`).
92 fn authorize(
93 &self,
94 source: &dyn AclSource,
95 req: &AuthorizationRequest<'_>,
96 ) -> AuthorizationResult;
97}
98
99/// Batch-authorize a set of topic names against the same principal /
100/// host / operation. Used by `Produce`, `Fetch`, and `Metadata`
101/// per-topic enforcement. The returned map's keys are borrowed from
102/// the input iterator so callers can avoid copying topic strings.
103#[must_use]
104pub fn authorize_topics<'a>(
105 authorizer: &dyn Authorizer,
106 source: &dyn AclSource,
107 principal: &Principal,
108 host: &SocketAddr,
109 operation: AclOperation,
110 topic_names: impl IntoIterator<Item = &'a str>,
111) -> std::collections::HashMap<&'a str, AuthorizationResult> {
112 topic_names
113 .into_iter()
114 .map(|name| {
115 let req = AuthorizationRequest {
116 principal,
117 host,
118 resource_type: ResourceType::Topic,
119 resource_name: name,
120 operation,
121 };
122 (name, authorizer.authorize(source, &req))
123 })
124 .collect()
125}