use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ResourceType {
Topic,
Group,
Cluster,
TransactionalId,
DelegationToken,
User,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PatternType {
Literal,
Prefixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PermissionType {
Allow,
Deny,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AclOperation {
All,
Read,
Write,
Create,
Delete,
Alter,
Describe,
ClusterAction,
DescribeConfigs,
AlterConfigs,
IdempotentWrite,
TwoPhaseCommit,
CreateTokens,
DescribeTokens,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AclEntry {
pub resource_type: ResourceType,
pub resource_name: String,
pub pattern_type: PatternType,
pub principal: String,
pub host: String,
pub operation: AclOperation,
pub permission_type: PermissionType,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AclEntryFilter {
pub resource_type: Option<ResourceType>,
pub resource_name: Option<String>,
pub pattern_type: Option<PatternType>,
pub principal: Option<String>,
pub host: Option<String>,
pub operation: Option<AclOperation>,
pub permission_type: Option<PermissionType>,
}
impl AclEntryFilter {
#[must_use]
pub fn matches(&self, entry: &AclEntry) -> bool {
self.resource_type
.is_none_or(|rt| rt == entry.resource_type)
&& self
.resource_name
.as_ref()
.is_none_or(|rn| rn == &entry.resource_name)
&& self.pattern_type.is_none_or(|pt| pt == entry.pattern_type)
&& self
.principal
.as_ref()
.is_none_or(|p| p == &entry.principal)
&& self.host.as_ref().is_none_or(|h| h == &entry.host)
&& self.operation.is_none_or(|op| op == entry.operation)
&& self
.permission_type
.is_none_or(|pt| pt == entry.permission_type)
}
}
#[cfg(test)]
mod tests {
use serde_wincode::SerdeCompat;
use wincode::{Deserialize as _, Serialize as _};
use super::*;
fn rt<T>(value: &T) -> T
where
T: Serialize + for<'de> Deserialize<'de> + PartialEq + std::fmt::Debug,
{
let bytes = <SerdeCompat<T>>::serialize(value).unwrap();
<SerdeCompat<T>>::deserialize(&bytes).unwrap()
}
#[test]
fn acl_entry_round_trip() {
let entry = AclEntry {
resource_type: ResourceType::Topic,
resource_name: "foo".into(),
pattern_type: PatternType::Literal,
principal: "User:alice".into(),
host: "*".into(),
operation: AclOperation::Read,
permission_type: PermissionType::Allow,
};
assert2::assert!(rt(&entry) == entry);
}
#[test]
fn acl_enum_log_encoding_is_stable() {
let index = |i: u32| i.to_le_bytes().to_vec();
let resource_types = [
(ResourceType::Topic, 0),
(ResourceType::Group, 1),
(ResourceType::Cluster, 2),
(ResourceType::TransactionalId, 3),
(ResourceType::DelegationToken, 4),
(ResourceType::User, 5),
];
for (variant, want) in resource_types {
let bytes = <SerdeCompat<ResourceType>>::serialize(&variant).unwrap();
assert2::assert!((variant, bytes) == (variant, index(want)));
}
let operations = [
(AclOperation::All, 0),
(AclOperation::Read, 1),
(AclOperation::Write, 2),
(AclOperation::Create, 3),
(AclOperation::Delete, 4),
(AclOperation::Alter, 5),
(AclOperation::Describe, 6),
(AclOperation::ClusterAction, 7),
(AclOperation::DescribeConfigs, 8),
(AclOperation::AlterConfigs, 9),
(AclOperation::IdempotentWrite, 10),
(AclOperation::TwoPhaseCommit, 11),
(AclOperation::CreateTokens, 12),
(AclOperation::DescribeTokens, 13),
];
for (variant, want) in operations {
let bytes = <SerdeCompat<AclOperation>>::serialize(&variant).unwrap();
assert2::assert!((variant, bytes) == (variant, index(want)));
}
for (variant, want) in [(PatternType::Literal, 0), (PatternType::Prefixed, 1)] {
let bytes = <SerdeCompat<PatternType>>::serialize(&variant).unwrap();
assert2::assert!((variant, bytes) == (variant, index(want)));
}
for (variant, want) in [(PermissionType::Allow, 0), (PermissionType::Deny, 1)] {
let bytes = <SerdeCompat<PermissionType>>::serialize(&variant).unwrap();
assert2::assert!((variant, bytes) == (variant, index(want)));
}
}
#[test]
fn kip_373_entries_round_trip() {
for operation in [AclOperation::CreateTokens, AclOperation::DescribeTokens] {
let entry = AclEntry {
resource_type: ResourceType::User,
resource_name: "alice".into(),
pattern_type: PatternType::Literal,
principal: "User:bob".into(),
host: "*".into(),
operation,
permission_type: PermissionType::Allow,
};
assert2::assert!(rt(&entry) == entry);
}
}
#[test]
fn acl_entry_filter_round_trip() {
let filter = AclEntryFilter {
resource_type: Some(ResourceType::Group),
resource_name: None,
pattern_type: Some(PatternType::Prefixed),
principal: Some("User:bob".into()),
host: None,
operation: Some(AclOperation::All),
permission_type: None,
};
assert2::assert!(rt(&filter) == filter);
}
#[test]
fn filter_with_all_none_matches_anything() {
let f = AclEntryFilter::default();
let entry = AclEntry {
resource_type: ResourceType::Cluster,
resource_name: "kafka-cluster".into(),
pattern_type: PatternType::Literal,
principal: "User:admin".into(),
host: "*".into(),
operation: AclOperation::All,
permission_type: PermissionType::Allow,
};
assert2::assert!(f.matches(&entry));
}
#[test]
fn filter_with_specific_axis_filters_correctly() {
let f = AclEntryFilter {
resource_type: Some(ResourceType::Topic),
..AclEntryFilter::default()
};
let topic_entry = AclEntry {
resource_type: ResourceType::Topic,
resource_name: "foo".into(),
pattern_type: PatternType::Literal,
principal: "User:alice".into(),
host: "*".into(),
operation: AclOperation::Read,
permission_type: PermissionType::Allow,
};
let group_entry = AclEntry {
resource_type: ResourceType::Group,
..topic_entry.clone()
};
assert2::assert!((f.matches(&topic_entry), f.matches(&group_entry)) == (true, false));
}
}