hodei_authz_sdk/
builder.rs1use 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#[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
40pub 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
51pub 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 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 #[cfg(feature = "postgres")]
84 pub fn with_postgres(mut self, pool: PgPool) -> Self {
85 self.postgres_pool = Some(pool);
86 self
87 }
88
89 #[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 pub fn auto_discover_schema(mut self) -> Result<Self, SchemaError> {
101 self.schema = Some(auto_discover_schema()?);
102 Ok(self)
103 }
104
105 pub fn with_schema(mut self, schema: Schema) -> Self {
107 self.schema = Some(schema);
108 self
109 }
110
111 #[cfg(feature = "postgres")]
113 pub fn without_auto_migrate(mut self) -> Self {
114 self.auto_migrate = false;
115 self
116 }
117
118 #[cfg(all(feature = "postgres", feature = "redis"))]
120 pub async fn build(self) -> Result<HodeiAuthService, BuildError> {
121 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 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 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 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 #[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 pub fn builder() -> HodeiAuthServiceBuilder {
177 HodeiAuthServiceBuilder::new()
178 }
179
180 pub fn schema(&self) -> &Schema {
182 &self.schema
183 }
184
185 #[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 #[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}