hodei_authz_sdk/
schema.rs

1//! Auto-discovery de schema Cedar usando inventory
2//!
3//! Este módulo recolecta automáticamente todos los EntitySchemaFragment y ActionSchemaFragment
4//! registrados por los derives HodeiEntity y HodeiAction.
5
6use cedar_policy::Schema;
7use hodei_authz::{ActionSchemaFragment, EntitySchemaFragment};
8use serde_json::{json, Value};
9use std::collections::HashMap;
10
11/// Error al generar el schema
12#[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
20/// Genera el schema Cedar automáticamente desde los fragments registrados con inventory
21///
22/// # Ejemplo
23///
24/// ```rust,ignore
25/// use hodei_authz_sdk::schema::auto_discover_schema;
26///
27/// let schema = auto_discover_schema()?;
28/// ```
29pub fn auto_discover_schema() -> Result<Schema, SchemaError> {
30    let mut namespaces: HashMap<String, Value> = HashMap::new();
31    
32    // Recolectar entity fragments
33    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    // Recolectar action fragments
39    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    // Construir el schema final
45    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
53/// Merge un EntitySchemaFragment en el schema
54fn merge_entity_fragment(
55    namespaces: &mut HashMap<String, Value>,
56    fragment: &EntitySchemaFragment,
57) {
58    // Parsear el entity_type para extraer namespace y entity name
59    // Formato esperado: "Namespace::EntityName"
60    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    // Obtener o crear el namespace
70    let ns = namespaces
71        .entry(namespace.to_string())
72        .or_insert_with(|| {
73            json!({
74                "entityTypes": {},
75                "actions": {}
76            })
77        });
78    
79    // Agregar el entity type
80    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
91/// Merge un ActionSchemaFragment en el schema
92fn merge_action_fragment(
93    namespaces: &mut HashMap<String, Value>,
94    fragment: &ActionSchemaFragment,
95) {
96    // Parsear el name para extraer namespace
97    // Formato esperado: "Namespace::ActionName" o "Namespace::Resource::ActionName"
98    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    // Obtener o crear el namespace
108    let ns = namespaces
109        .entry(namespace.to_string())
110        .or_insert_with(|| {
111            json!({
112                "entityTypes": {},
113                "actions": {}
114            })
115        });
116    
117    // Agregar la action
118    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
129/// Genera un schema de ejemplo para testing
130pub 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}