Skip to main content

hawdb_core/
access_control.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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}