hodei_authz_sdk/
builder.rs

1//! Builder pattern para configurar HodeiAuthService fácilmente
2
3use crate::schema::{auto_discover_schema, SchemaError};
4use cedar_policy::{Authorizer, PolicySet, Schema};
5use hodei_authz::{CacheInvalidation, PolicyStore};
6
7#[cfg(feature = "postgres")]
8use hodei_authz_postgres::PostgresPolicyStore;
9#[cfg(feature = "postgres")]
10use sqlx::PgPool;
11
12#[cfg(feature = "redis")]
13use hodei_authz_redis::RedisCacheInvalidation;
14
15use std::sync::Arc;
16use tokio::sync::RwLock;
17
18/// Error al construir el servicio
19#[derive(Debug, thiserror::Error)]
20pub enum BuildError {
21    #[error("PostgreSQL pool is required")]
22    MissingPostgres,
23    
24    #[error("Redis URL is required")]
25    MissingRedis,
26    
27    #[error("Schema error: {0}")]
28    Schema(#[from] SchemaError),
29    
30    #[error("Policy store error: {0}")]
31    PolicyStore(String),
32    
33    #[error("Cache error: {0}")]
34    Cache(String),
35    
36    #[error("Migration error: {0}")]
37    Migration(String),
38}
39
40/// Servicio de autorización completo
41pub struct HodeiAuthService {
42    #[cfg(feature = "postgres")]
43    pub(crate) policy_store: Arc<PostgresPolicyStore>,
44    #[cfg(feature = "redis")]
45    pub(crate) cache_invalidation: Arc<RedisCacheInvalidation>,
46    pub(crate) authorizer: Authorizer,
47    pub(crate) schema: Arc<Schema>,
48    pub(crate) policy_set: Arc<RwLock<PolicySet>>,
49}
50
51/// Builder para HodeiAuthService
52pub struct HodeiAuthServiceBuilder {
53    #[cfg(feature = "postgres")]
54    postgres_pool: Option<PgPool>,
55    #[cfg(feature = "redis")]
56    redis_url: Option<String>,
57    schema: Option<Schema>,
58    #[cfg(feature = "postgres")]
59    auto_migrate: bool,
60}
61
62impl Default for HodeiAuthServiceBuilder {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68impl HodeiAuthServiceBuilder {
69    /// Crea un nuevo builder
70    pub fn new() -> Self {
71        Self {
72            #[cfg(feature = "postgres")]
73            postgres_pool: None,
74            #[cfg(feature = "redis")]
75            redis_url: None,
76            schema: None,
77            #[cfg(feature = "postgres")]
78            auto_migrate: true,
79        }
80    }
81    
82    /// Configura el pool de PostgreSQL
83    #[cfg(feature = "postgres")]
84    pub fn with_postgres(mut self, pool: PgPool) -> Self {
85        self.postgres_pool = Some(pool);
86        self
87    }
88    
89    /// Configura la URL de Redis
90    #[cfg(feature = "redis")]
91    pub fn with_redis(mut self, url: impl Into<String>) -> Self {
92        self.redis_url = Some(url.into());
93        self
94    }
95    
96    /// Auto-descubre el schema usando inventory
97    ///
98    /// Esto recolecta todos los EntitySchemaFragment y ActionSchemaFragment
99    /// registrados por los derives HodeiEntity y HodeiAction.
100    pub fn auto_discover_schema(mut self) -> Result<Self, SchemaError> {
101        self.schema = Some(auto_discover_schema()?);
102        Ok(self)
103    }
104    
105    /// Usa un schema personalizado
106    pub fn with_schema(mut self, schema: Schema) -> Self {
107        self.schema = Some(schema);
108        self
109    }
110    
111    /// Deshabilita las migraciones automáticas
112    #[cfg(feature = "postgres")]
113    pub fn without_auto_migrate(mut self) -> Self {
114        self.auto_migrate = false;
115        self
116    }
117    
118    /// Construye el servicio
119    #[cfg(all(feature = "postgres", feature = "redis"))]
120    pub async fn build(self) -> Result<HodeiAuthService, BuildError> {
121        // Validar configuración
122        let pool = self.postgres_pool.ok_or(BuildError::MissingPostgres)?;
123        let redis_url = self.redis_url.ok_or(BuildError::MissingRedis)?;
124        let schema = self.schema.ok_or_else(|| {
125            SchemaError::InvalidStructure(
126                "Schema is required. Call auto_discover_schema() or with_schema()".to_string()
127            )
128        })?;
129        
130        // Setup policy store
131        let policy_store = PostgresPolicyStore::new(pool);
132        
133        if self.auto_migrate {
134            policy_store
135                .migrate()
136                .await
137                .map_err(|e| BuildError::Migration(e.to_string()))?;
138            tracing::info!("✅ Database migrations completed");
139        }
140        
141        // Setup cache invalidation
142        let cache_invalidation = RedisCacheInvalidation::new(&redis_url)
143            .await
144            .map_err(|e| BuildError::Cache(e.to_string()))?;
145        tracing::info!("✅ Redis cache connected");
146        
147        // Load policies
148        let policy_set = Self::load_initial_policies(&policy_store).await?;
149        tracing::info!("✅ Policies loaded");
150        
151        let authorizer = Authorizer::new();
152        
153        Ok(HodeiAuthService {
154            policy_store: Arc::new(policy_store),
155            cache_invalidation: Arc::new(cache_invalidation),
156            authorizer,
157            schema: Arc::new(schema),
158            policy_set: Arc::new(RwLock::new(policy_set)),
159        })
160    }
161    
162    /// Carga las políticas iniciales
163    #[cfg(feature = "postgres")]
164    async fn load_initial_policies(
165        store: &PostgresPolicyStore,
166    ) -> Result<PolicySet, BuildError> {
167        store
168            .load_all_policies()
169            .await
170            .map_err(|e| BuildError::PolicyStore(e.to_string()))
171    }
172}
173
174impl HodeiAuthService {
175    /// Crea un nuevo builder
176    pub fn builder() -> HodeiAuthServiceBuilder {
177        HodeiAuthServiceBuilder::new()
178    }
179    
180    /// Obtiene el schema
181    pub fn schema(&self) -> &Schema {
182        &self.schema
183    }
184    
185    /// Recarga las políticas
186    #[cfg(feature = "postgres")]
187    pub async fn reload_policies(&self) -> Result<(), BuildError> {
188        let new_policy_set = self
189            .policy_store
190            .load_all_policies()
191            .await
192            .map_err(|e| BuildError::PolicyStore(e.to_string()))?;
193        
194        let mut policy_set = self.policy_set.write().await;
195        *policy_set = new_policy_set;
196        
197        Ok(())
198    }
199    
200    /// Invalida el caché
201    #[cfg(feature = "redis")]
202    pub async fn invalidate_cache(&self) -> Result<(), BuildError> {
203        self.cache_invalidation
204            .invalidate_policies()
205            .await
206            .map_err(|e| BuildError::Cache(e.to_string()))
207    }
208}