Skip to main content

krabka_admin_ui/
permissions.rs

1use krabka_client_admin::{AclEntry, AclOperation, PermissionType, ResourceType};
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
4pub struct Capabilities(u16);
5
6impl Capabilities {
7    const VIEW_TOPICS: u16 = 1 << 0;
8    const CREATE_TOPICS: u16 = 1 << 1;
9    const ALTER_TOPICS: u16 = 1 << 2;
10    const DELETE_TOPICS: u16 = 1 << 3;
11    const VIEW_GROUPS: u16 = 1 << 4;
12    const VIEW_ACLS: u16 = 1 << 5;
13    const ALTER_ACLS: u16 = 1 << 6;
14    const ALTER_USERS: u16 = 1 << 7;
15    const VIEW_QUOTAS: u16 = 1 << 8;
16    const ALTER_QUOTAS: u16 = 1 << 9;
17    const VIEW_LOG_DIRS: u16 = 1 << 10;
18    const VIEW_USERS: u16 = 1 << 11;
19    const ALTER_LOG_DIRS: u16 = 1 << 12;
20    /// Every capability this UI knows about.
21    const EVERY: u16 = (1 << 13) - 1;
22
23    /// The capabilities of an operator whose ACLs permit everything.
24    ///
25    /// The UI also uses this when it cannot read the operator's ACLs: it then
26    /// shows every page and control and lets the broker refuse what the
27    /// operator may not do.
28    #[must_use]
29    pub const fn all() -> Self {
30        Self(Self::EVERY)
31    }
32
33    /// The capabilities of an operator whose ACLs permit nothing.
34    #[must_use]
35    pub const fn none() -> Self {
36        Self(0)
37    }
38
39    const fn contains(self, capability: u16) -> bool {
40        self.0 & capability != 0
41    }
42
43    #[must_use]
44    pub const fn can_view_topics(self) -> bool {
45        self.contains(Self::VIEW_TOPICS)
46    }
47    #[must_use]
48    pub const fn can_create_topics(self) -> bool {
49        self.contains(Self::CREATE_TOPICS)
50    }
51    #[must_use]
52    pub const fn can_alter_topics(self) -> bool {
53        self.contains(Self::ALTER_TOPICS)
54    }
55    #[must_use]
56    pub const fn can_delete_topics(self) -> bool {
57        self.contains(Self::DELETE_TOPICS)
58    }
59    #[must_use]
60    pub const fn can_view_groups(self) -> bool {
61        self.contains(Self::VIEW_GROUPS)
62    }
63    #[must_use]
64    pub const fn can_view_acls(self) -> bool {
65        self.contains(Self::VIEW_ACLS)
66    }
67    #[must_use]
68    pub const fn can_alter_acls(self) -> bool {
69        self.contains(Self::ALTER_ACLS)
70    }
71    #[must_use]
72    pub const fn can_alter_users(self) -> bool {
73        self.contains(Self::ALTER_USERS)
74    }
75    #[must_use]
76    pub const fn can_view_quotas(self) -> bool {
77        self.contains(Self::VIEW_QUOTAS)
78    }
79    #[must_use]
80    pub const fn can_alter_quotas(self) -> bool {
81        self.contains(Self::ALTER_QUOTAS)
82    }
83    #[must_use]
84    pub const fn can_view_log_dirs(self) -> bool {
85        self.contains(Self::VIEW_LOG_DIRS)
86    }
87    #[must_use]
88    pub const fn can_view_users(self) -> bool {
89        self.contains(Self::VIEW_USERS)
90    }
91    #[must_use]
92    pub const fn can_alter_log_dirs(self) -> bool {
93        self.contains(Self::ALTER_LOG_DIRS)
94    }
95}
96
97#[must_use]
98pub fn derive_capabilities(principal: &str, entries: &[AclEntry]) -> Capabilities {
99    derive_capabilities_for_optional_host(principal, None, entries)
100}
101
102#[must_use]
103pub fn derive_capabilities_for_host(
104    principal: &str,
105    peer_host: &str,
106    entries: &[AclEntry],
107) -> Capabilities {
108    derive_capabilities_for_optional_host(principal, Some(peer_host), entries)
109}
110
111fn derive_capabilities_for_optional_host(
112    principal: &str,
113    peer_host: Option<&str>,
114    entries: &[AclEntry],
115) -> Capabilities {
116    let effective = |resource_type, operation| {
117        has_effective_permission(principal, peer_host, entries, resource_type, operation)
118    };
119
120    let mut bits = 0;
121    let mut grant = |capability, granted| {
122        if granted {
123            bits |= capability;
124        }
125    };
126    grant(
127        Capabilities::VIEW_TOPICS,
128        effective(ResourceType::Topic, AclOperation::Describe),
129    );
130    grant(
131        Capabilities::CREATE_TOPICS,
132        effective(ResourceType::Cluster, AclOperation::Create),
133    );
134    grant(
135        Capabilities::ALTER_TOPICS,
136        effective(ResourceType::Topic, AclOperation::Alter)
137            || effective(ResourceType::Topic, AclOperation::AlterConfigs),
138    );
139    grant(
140        Capabilities::DELETE_TOPICS,
141        effective(ResourceType::Topic, AclOperation::Delete),
142    );
143    grant(
144        Capabilities::VIEW_GROUPS,
145        effective(ResourceType::Group, AclOperation::Describe),
146    );
147    grant(
148        Capabilities::VIEW_ACLS,
149        effective(ResourceType::Cluster, AclOperation::Describe),
150    );
151    grant(
152        Capabilities::ALTER_ACLS,
153        effective(ResourceType::Cluster, AclOperation::Alter),
154    );
155    grant(
156        Capabilities::ALTER_USERS,
157        effective(ResourceType::Cluster, AclOperation::Alter),
158    );
159    grant(
160        Capabilities::VIEW_QUOTAS,
161        effective(ResourceType::Cluster, AclOperation::Describe),
162    );
163    grant(
164        Capabilities::ALTER_QUOTAS,
165        effective(ResourceType::Cluster, AclOperation::Alter),
166    );
167    grant(
168        Capabilities::VIEW_LOG_DIRS,
169        effective(ResourceType::Cluster, AclOperation::Describe),
170    );
171    grant(
172        Capabilities::VIEW_USERS,
173        effective(ResourceType::Cluster, AclOperation::Describe),
174    );
175    grant(
176        Capabilities::ALTER_LOG_DIRS,
177        effective(ResourceType::Cluster, AclOperation::Alter),
178    );
179    Capabilities(bits)
180}
181
182fn is_for_principal_and_host(entry: &AclEntry, principal: &str, peer_host: Option<&str>) -> bool {
183    is_for_principal(entry, principal) && is_for_host(entry, peer_host)
184}
185
186fn is_for_principal(entry: &AclEntry, principal: &str) -> bool {
187    entry.principal == principal || entry.principal == "User:*"
188}
189
190fn is_for_host(entry: &AclEntry, peer_host: Option<&str>) -> bool {
191    if entry.host == "*" {
192        return true;
193    }
194
195    peer_host.is_some_and(|host| entry.host == host)
196}
197
198fn has_effective_permission(
199    principal: &str,
200    peer_host: Option<&str>,
201    entries: &[AclEntry],
202    resource_type: ResourceType,
203    operation: AclOperation,
204) -> bool {
205    let mut saw_allow = false;
206
207    for entry in entries {
208        if entry.resource_type != resource_type
209            || !is_for_principal_and_host(entry, principal, peer_host)
210            || !matches_operation(entry.operation, operation)
211        {
212            continue;
213        }
214
215        match entry.permission_type {
216            PermissionType::Allow => saw_allow = true,
217            PermissionType::Deny => return false,
218        }
219    }
220
221    saw_allow
222}
223
224fn matches_operation(stored: AclOperation, requested: AclOperation) -> bool {
225    stored == requested || matches!(stored, AclOperation::All) || implies(stored, requested)
226}
227
228fn implies(stored: AclOperation, requested: AclOperation) -> bool {
229    matches!(
230        (stored, requested),
231        (
232            AclOperation::Read | AclOperation::Write | AclOperation::Delete | AclOperation::Alter,
233            AclOperation::Describe,
234        ) | (AclOperation::AlterConfigs, AclOperation::DescribeConfigs)
235    )
236}