hodei_authz_sdk/
schema.rs1use cedar_policy::Schema;
7use hodei_authz::{ActionSchemaFragment, EntitySchemaFragment};
8use serde_json::{json, Value};
9use std::collections::HashMap;
10
11#[derive(Debug, thiserror::Error)]
13pub enum SchemaError {
14 #[error("Error parsing schema: {0}")]
15 ParseError(String),
16 #[error("Invalid schema structure: {0}")]
17 InvalidStructure(String),
18}
19
20pub fn auto_discover_schema() -> Result<Schema, SchemaError> {
30 let mut namespaces: HashMap<String, Value> = HashMap::new();
31
32 for fragment in hodei_authz::inventory::iter::<EntitySchemaFragment>() {
34 tracing::debug!("Discovered entity: {}", fragment.entity_type);
35 merge_entity_fragment(&mut namespaces, fragment);
36 }
37
38 for fragment in hodei_authz::inventory::iter::<ActionSchemaFragment>() {
40 tracing::debug!("Discovered action: {}", fragment.name);
41 merge_action_fragment(&mut namespaces, fragment);
42 }
43
44 let schema_json = json!(namespaces);
46
47 tracing::info!("Generated schema with {} namespaces", namespaces.len());
48
49 Schema::from_json_str(&schema_json.to_string())
50 .map_err(|e| SchemaError::ParseError(e.to_string()))
51}
52
53fn merge_entity_fragment(
55 namespaces: &mut HashMap<String, Value>,
56 fragment: &EntitySchemaFragment,
57) {
58 let parts: Vec<&str> = fragment.entity_type.split("::").collect();
61 if parts.len() != 2 {
62 tracing::warn!("Invalid entity_type format: {}", fragment.entity_type);
63 return;
64 }
65
66 let namespace = parts[0];
67 let entity_name = parts[1];
68
69 let ns = namespaces
71 .entry(namespace.to_string())
72 .or_insert_with(|| {
73 json!({
74 "entityTypes": {},
75 "actions": {}
76 })
77 });
78
79 if let Some(entity_types) = ns.get_mut("entityTypes") {
81 if let Some(obj) = entity_types.as_object_mut() {
82 obj.insert(
83 entity_name.to_string(),
84 serde_json::from_str(&fragment.fragment_json)
85 .unwrap_or_else(|_| json!({})),
86 );
87 }
88 }
89}
90
91fn merge_action_fragment(
93 namespaces: &mut HashMap<String, Value>,
94 fragment: &ActionSchemaFragment,
95) {
96 let parts: Vec<&str> = fragment.name.split("::").collect();
99 if parts.is_empty() {
100 tracing::warn!("Invalid action name format: {}", fragment.name);
101 return;
102 }
103
104 let namespace = parts[0];
105 let action_key = parts[1..].join("::");
106
107 let ns = namespaces
109 .entry(namespace.to_string())
110 .or_insert_with(|| {
111 json!({
112 "entityTypes": {},
113 "actions": {}
114 })
115 });
116
117 if let Some(actions) = ns.get_mut("actions") {
119 if let Some(obj) = actions.as_object_mut() {
120 obj.insert(
121 action_key,
122 serde_json::from_str(&fragment.fragment_json)
123 .unwrap_or_else(|_| json!({})),
124 );
125 }
126 }
127}
128
129pub fn example_schema() -> Result<Schema, SchemaError> {
131 let schema_json = json!({
132 "DocApp": {
133 "entityTypes": {
134 "User": {
135 "shape": {
136 "type": "Record",
137 "attributes": {
138 "email": { "type": "String" },
139 "name": { "type": "String" },
140 "role": { "type": "String" }
141 }
142 }
143 },
144 "Document": {
145 "shape": {
146 "type": "Record",
147 "attributes": {
148 "owner_id": { "type": "Entity", "name": "User" },
149 "title": { "type": "String" },
150 "content": { "type": "String" },
151 "is_public": { "type": "Boolean" }
152 }
153 }
154 }
155 },
156 "actions": {
157 "Document::Read": {
158 "appliesTo": {
159 "principalTypes": ["User"],
160 "resourceTypes": ["Document"]
161 }
162 },
163 "Document::Update": {
164 "appliesTo": {
165 "principalTypes": ["User"],
166 "resourceTypes": ["Document"]
167 }
168 }
169 }
170 }
171 });
172
173 Schema::from_json_str(&schema_json.to_string())
174 .map_err(|e| SchemaError::ParseError(e.to_string()))
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_example_schema() {
183 let schema = example_schema();
184 assert!(schema.is_ok());
185 }
186}