1use std::fmt;
11
12use corium_protocol::authz::{Action, ActionClass};
13
14pub const WILDCARD: &str = "*";
16
17#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct ObjectRef {
23 pub kind: String,
25 pub id: String,
27}
28
29impl ObjectRef {
30 pub fn new(kind: impl Into<String>, id: impl Into<String>) -> Self {
32 Self {
33 kind: kind.into(),
34 id: id.into(),
35 }
36 }
37
38 #[must_use]
41 pub fn parse(text: &str) -> Self {
42 match text.split_once(':') {
43 Some((kind, id)) if !kind.is_empty() && !id.is_empty() => Self::new(kind, id),
44 _ => Self::new("user", text),
45 }
46 }
47
48 #[must_use]
50 pub fn wildcard_of_type(&self) -> Self {
51 Self::new(self.kind.clone(), WILDCARD)
52 }
53
54 #[must_use]
56 pub fn is_wildcard(&self) -> bool {
57 self.id == WILDCARD
58 }
59}
60
61impl fmt::Display for ObjectRef {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write!(formatter, "{}:{}", self.kind, self.id)
64 }
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
75pub struct SubjectRef {
76 pub object: ObjectRef,
78 pub relation: Option<String>,
80}
81
82impl SubjectRef {
83 #[must_use]
85 pub fn parse(text: &str) -> Self {
86 match text.split_once('#') {
87 Some((object, relation)) if !relation.is_empty() => Self {
88 object: ObjectRef::parse(object),
89 relation: Some(relation.to_owned()),
90 },
91 _ => Self {
92 object: ObjectRef::parse(text),
93 relation: None,
94 },
95 }
96 }
97}
98
99impl fmt::Display for SubjectRef {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 match &self.relation {
102 Some(relation) => write!(formatter, "{}#{relation}", self.object),
103 None => write!(formatter, "{}", self.object),
104 }
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct Tuple {
111 pub subject: SubjectRef,
113 pub relation: String,
115 pub object: ObjectRef,
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
127pub struct Rewrite {
128 pub relation: String,
130 pub via_relation: String,
132 pub on_relation: String,
134 pub object_type: Option<String>,
136}
137
138#[derive(Clone, Debug, PartialEq, Eq)]
144pub struct Permission {
145 pub object_type: String,
147 pub action: String,
149 pub relations: Vec<String>,
151}
152
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
155pub enum FilterKind {
156 AttributeAllowlist,
158 AttributeDenylist,
160}
161
162impl FilterKind {
163 #[must_use]
165 pub fn parse(text: &str) -> Option<Self> {
166 match text {
167 "attribute-allowlist" | "allowlist" => Some(Self::AttributeAllowlist),
168 "attribute-denylist" | "denylist" => Some(Self::AttributeDenylist),
169 _ => None,
170 }
171 }
172}
173
174#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct ViewDef {
178 pub name: String,
180 pub kind: FilterKind,
182 pub attributes: Vec<String>,
184}
185
186#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct Binding {
190 pub relation: String,
192 pub object: ObjectRef,
194 pub view: Option<String>,
196 pub unfiltered: bool,
199}
200
201#[derive(Clone, Debug, Default, PartialEq, Eq)]
204pub struct PrincipalDef {
205 pub id: String,
207 pub provider: Option<String>,
209 pub roles: Vec<String>,
211}
212
213#[derive(Clone, Debug, PartialEq, Eq)]
217pub struct ObjectDef {
218 pub object: ObjectRef,
220 pub database: Option<String>,
222}
223
224#[must_use]
226pub fn action_name(action: Action) -> &'static str {
227 match action {
228 Action::Query => "query",
229 Action::Pull => "pull",
230 Action::Datoms => "datoms",
231 Action::TxRange => "tx-range",
232 Action::Subscribe => "subscribe",
233 Action::Inspect => "inspect",
234 Action::Transact => "transact",
235 Action::CreateDatabase => "create-database",
236 Action::DeleteDatabase => "delete-database",
237 Action::ForkDatabase => "fork-database",
238 Action::ListDatabases => "list-databases",
239 Action::GarbageCollect => "garbage-collect",
240 Action::ManageIndex => "manage-index",
241 Action::ManageKeys => "manage-keys",
242 }
243}
244
245#[must_use]
248pub fn action_from_name(name: &str) -> Option<Action> {
249 const ACTIONS: [Action; 14] = [
250 Action::Query,
251 Action::Pull,
252 Action::Datoms,
253 Action::TxRange,
254 Action::Subscribe,
255 Action::Inspect,
256 Action::Transact,
257 Action::CreateDatabase,
258 Action::DeleteDatabase,
259 Action::ForkDatabase,
260 Action::ListDatabases,
261 Action::GarbageCollect,
262 Action::ManageIndex,
263 Action::ManageKeys,
264 ];
265 ACTIONS
266 .into_iter()
267 .find(|action| action_name(*action) == name)
268}
269
270#[must_use]
272pub fn action_names() -> Vec<&'static str> {
273 [
274 Action::Query,
275 Action::Pull,
276 Action::Datoms,
277 Action::TxRange,
278 Action::Subscribe,
279 Action::Inspect,
280 Action::Transact,
281 Action::CreateDatabase,
282 Action::DeleteDatabase,
283 Action::ForkDatabase,
284 Action::ListDatabases,
285 Action::GarbageCollect,
286 Action::ManageIndex,
287 Action::ManageKeys,
288 ]
289 .into_iter()
290 .map(action_name)
291 .collect()
292}
293
294#[must_use]
296pub fn action_class_name(class: ActionClass) -> &'static str {
297 match class {
298 ActionClass::Read => "read",
299 ActionClass::Write => "write",
300 ActionClass::Admin => "admin",
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn object_refs_round_trip() {
310 let reference = ObjectRef::parse("database:music");
311 assert_eq!(reference, ObjectRef::new("database", "music"));
312 assert_eq!(reference.to_string(), "database:music");
313 assert_eq!(
314 reference.wildcard_of_type(),
315 ObjectRef::new("database", "*")
316 );
317 assert!(reference.wildcard_of_type().is_wildcard());
318 assert_eq!(ObjectRef::parse("alice"), ObjectRef::new("user", "alice"));
320 assert_eq!(
321 ObjectRef::parse("user:oidc:alice"),
322 ObjectRef::new("user", "oidc:alice")
323 );
324 }
325
326 #[test]
327 fn subject_refs_carry_usersets() {
328 let plain = SubjectRef::parse("group:eng");
329 assert_eq!(plain.relation, None);
330 let userset = SubjectRef::parse("group:eng#member");
331 assert_eq!(userset.object, ObjectRef::new("group", "eng"));
332 assert_eq!(userset.relation.as_deref(), Some("member"));
333 assert_eq!(userset.to_string(), "group:eng#member");
334 }
335
336 #[test]
337 fn action_names_are_stable() {
338 assert_eq!(action_name(Action::Query), "query");
339 assert_eq!(action_name(Action::CreateDatabase), "create-database");
340 assert_eq!(action_class_name(ActionClass::Admin), "admin");
341 }
342
343 #[test]
344 fn action_names_round_trip() {
345 for name in action_names() {
346 let action = action_from_name(name).expect("every listed name parses");
347 assert_eq!(action_name(action), name);
348 }
349 assert_eq!(action_names().len(), 14);
350 assert!(action_from_name("nonsense").is_none());
351 }
352}