hawdb_core/
access_control.rs1use crate::{HawDBError, Result};
16use std::collections::BTreeSet;
17
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
19pub struct QueryAccessControlContext {
20 policy_epoch: u64,
21 visibility_property: String,
22 allowed_visibility_values: BTreeSet<String>,
23}
24
25impl QueryAccessControlContext {
26 pub fn visibility_scope(
27 policy_epoch: u64,
28 visibility_property: impl Into<String>,
29 allowed_visibility_value: impl Into<String>,
30 ) -> Self {
31 Self::visibility_scopes(
32 policy_epoch,
33 visibility_property,
34 std::iter::once(allowed_visibility_value),
35 )
36 }
37
38 pub fn visibility_scopes(
39 policy_epoch: u64,
40 visibility_property: impl Into<String>,
41 allowed_visibility_values: impl IntoIterator<Item = impl Into<String>>,
42 ) -> Self {
43 Self {
44 policy_epoch,
45 visibility_property: visibility_property.into(),
46 allowed_visibility_values: allowed_visibility_values
47 .into_iter()
48 .map(Into::into)
49 .collect(),
50 }
51 }
52
53 pub const fn policy_epoch(&self) -> u64 {
54 self.policy_epoch
55 }
56
57 pub fn visibility_property(&self) -> &str {
58 &self.visibility_property
59 }
60
61 pub fn allowed_visibility_values(&self) -> &BTreeSet<String> {
62 &self.allowed_visibility_values
63 }
64
65 pub fn validate(&self) -> Result<()> {
66 if self.policy_epoch == 0 {
67 return Err(HawDBError::Semantic(
68 "access control policy epoch must be non-zero".to_string(),
69 ));
70 }
71 if self.visibility_property.trim().is_empty() {
72 return Err(HawDBError::Semantic(
73 "access control visibility property must be non-empty".to_string(),
74 ));
75 }
76 if self.allowed_visibility_values.is_empty() {
77 return Err(HawDBError::Semantic(
78 "access control visibility scope must not be empty".to_string(),
79 ));
80 }
81 if self
82 .allowed_visibility_values
83 .iter()
84 .any(|value| value.trim().is_empty())
85 {
86 return Err(HawDBError::Semantic(
87 "access control visibility scope values must be non-empty".to_string(),
88 ));
89 }
90 Ok(())
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::QueryAccessControlContext;
97
98 #[test]
99 fn visibility_scopes_normalize_duplicate_values_and_expose_stable_order() {
100 let context = QueryAccessControlContext::visibility_scopes(
101 7,
102 "space_id",
103 ["team-b", "team-a", "team-b"],
104 );
105
106 assert_eq!(context.policy_epoch(), 7);
107 assert_eq!(context.visibility_property(), "space_id");
108 assert_eq!(
109 context
110 .allowed_visibility_values()
111 .iter()
112 .map(String::as_str)
113 .collect::<Vec<_>>(),
114 ["team-a", "team-b"]
115 );
116 assert!(context.validate().is_ok());
117 }
118
119 #[test]
120 fn validation_rejects_invalid_policy_inputs() {
121 let invalid_contexts = [
122 QueryAccessControlContext::visibility_scope(0, "space_id", "team-a"),
123 QueryAccessControlContext::visibility_scope(7, " ", "team-a"),
124 QueryAccessControlContext::visibility_scopes(7, "space_id", Vec::<String>::new()),
125 QueryAccessControlContext::visibility_scope(7, "space_id", " "),
126 ];
127
128 for context in invalid_contexts {
129 assert!(context.validate().is_err());
130 }
131 }
132}