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 }
242}
243
244#[must_use]
247pub fn action_from_name(name: &str) -> Option<Action> {
248 const ACTIONS: [Action; 13] = [
249 Action::Query,
250 Action::Pull,
251 Action::Datoms,
252 Action::TxRange,
253 Action::Subscribe,
254 Action::Inspect,
255 Action::Transact,
256 Action::CreateDatabase,
257 Action::DeleteDatabase,
258 Action::ForkDatabase,
259 Action::ListDatabases,
260 Action::GarbageCollect,
261 Action::ManageIndex,
262 ];
263 ACTIONS
264 .into_iter()
265 .find(|action| action_name(*action) == name)
266}
267
268#[must_use]
270pub fn action_names() -> Vec<&'static str> {
271 [
272 Action::Query,
273 Action::Pull,
274 Action::Datoms,
275 Action::TxRange,
276 Action::Subscribe,
277 Action::Inspect,
278 Action::Transact,
279 Action::CreateDatabase,
280 Action::DeleteDatabase,
281 Action::ForkDatabase,
282 Action::ListDatabases,
283 Action::GarbageCollect,
284 Action::ManageIndex,
285 ]
286 .into_iter()
287 .map(action_name)
288 .collect()
289}
290
291#[must_use]
293pub fn action_class_name(class: ActionClass) -> &'static str {
294 match class {
295 ActionClass::Read => "read",
296 ActionClass::Write => "write",
297 ActionClass::Admin => "admin",
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn object_refs_round_trip() {
307 let reference = ObjectRef::parse("database:music");
308 assert_eq!(reference, ObjectRef::new("database", "music"));
309 assert_eq!(reference.to_string(), "database:music");
310 assert_eq!(
311 reference.wildcard_of_type(),
312 ObjectRef::new("database", "*")
313 );
314 assert!(reference.wildcard_of_type().is_wildcard());
315 assert_eq!(ObjectRef::parse("alice"), ObjectRef::new("user", "alice"));
317 assert_eq!(
318 ObjectRef::parse("user:oidc:alice"),
319 ObjectRef::new("user", "oidc:alice")
320 );
321 }
322
323 #[test]
324 fn subject_refs_carry_usersets() {
325 let plain = SubjectRef::parse("group:eng");
326 assert_eq!(plain.relation, None);
327 let userset = SubjectRef::parse("group:eng#member");
328 assert_eq!(userset.object, ObjectRef::new("group", "eng"));
329 assert_eq!(userset.relation.as_deref(), Some("member"));
330 assert_eq!(userset.to_string(), "group:eng#member");
331 }
332
333 #[test]
334 fn action_names_are_stable() {
335 assert_eq!(action_name(Action::Query), "query");
336 assert_eq!(action_name(Action::CreateDatabase), "create-database");
337 assert_eq!(action_class_name(ActionClass::Admin), "admin");
338 }
339
340 #[test]
341 fn action_names_round_trip() {
342 for name in action_names() {
343 let action = action_from_name(name).expect("every listed name parses");
344 assert_eq!(action_name(action), name);
345 }
346 assert_eq!(action_names().len(), 13);
347 assert!(action_from_name("nonsense").is_none());
348 }
349}