1use std::collections::HashMap;
5use std::sync::{Arc, Mutex, OnceLock};
6use std::time::{Duration, Instant};
7
8use super::{AuthzError, ObjectRef, Subject, Tuple, TupleFilter, TupleStore};
9use crate::model::backend::Backend;
10use crate::model::{DbConfig, DbPool, Value};
11
12pub const READ_CACHE: Duration = Duration::from_secs(1);
14
15const TABLE: &str = "rws_authz_tuples";
16
17fn runtime() -> &'static tokio::runtime::Runtime {
20 static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
21 RUNTIME.get_or_init(|| {
22 tokio::runtime::Builder::new_multi_thread()
23 .worker_threads(2)
24 .thread_name("rws-authz-sql")
25 .enable_all()
26 .build()
27 .expect("the authorization store runtime")
28 })
29}
30
31#[doc(hidden)]
34pub fn run<T: Send + 'static>(f: impl std::future::Future<Output = T> + Send + 'static) -> Result<T, AuthzError> {
35 let (tx, rx) = std::sync::mpsc::sync_channel(1);
36 runtime().spawn(async move {
37 let _ = tx.send(f.await);
38 });
39 rx.recv().map_err(|_| AuthzError::Store("the query was dropped".to_string()))
40}
41
42fn store_error(e: impl std::fmt::Display) -> AuthzError {
43 AuthzError::Store(e.to_string())
44}
45
46pub struct SqlStore {
50 pool: DbPool,
51 cache: Mutex<HashMap<(ObjectRef, String), (Instant, Vec<Subject>)>>,
52}
53
54impl SqlStore {
55 pub fn open(config: DbConfig) -> Result<SqlStore, AuthzError> {
58 let pool = run(async move { DbPool::new(config).await })?.map_err(store_error)?;
59 SqlStore::new(pool)
60 }
61
62 pub fn from_env() -> Result<SqlStore, AuthzError> {
65 let config = DbConfig::from_env().map_err(store_error)?;
66 let tls = crate::model::DbTls::from_env().map_err(store_error)?;
67 let pool = run(async move { DbPool::new_with_tls(config, tls).await })?.map_err(store_error)?;
68 SqlStore::new(pool)
69 }
70
71 pub fn new(pool: DbPool) -> Result<SqlStore, AuthzError> {
74 let store = SqlStore { pool, cache: Mutex::default() };
75 let text = if matches!(store.pool.backend(), b if is_mysql(b)) { "VARCHAR(256) CHARACTER SET utf8mb4" } else { "VARCHAR(256)" };
76 let create = format!(
77 "CREATE TABLE IF NOT EXISTS {TABLE} (tuple_key CHAR(64) NOT NULL PRIMARY KEY, object_type {text} NOT NULL, \
78 object_id {text} NOT NULL, relation {text} NOT NULL, subject_type {text} NOT NULL, subject_id {text} NOT NULL, \
79 subject_relation {text} NOT NULL)"
80 );
81 store.execute(create, Vec::new())?;
82 let index = if is_mysql(store.pool.backend()) {
83 format!("CREATE INDEX {TABLE}_object ON {TABLE} (object_type(64), object_id(128), relation(64))")
85 } else {
86 format!("CREATE INDEX IF NOT EXISTS {TABLE}_object ON {TABLE} (object_type, object_id, relation)")
87 };
88 if let Err(e) = store.execute(index, Vec::new()) {
89 if !is_mysql(store.pool.backend()) {
90 return Err(e);
91 }
92 }
93 Ok(store)
94 }
95
96 fn execute(&self, sql: String, params: Vec<Value>) -> Result<u64, AuthzError> {
97 let pool = self.pool.clone();
98 run(async move { pool.execute(&sql, ¶ms).await })?.map_err(store_error)
99 }
100
101 fn query(&self, sql: String, params: Vec<Value>) -> Result<Vec<crate::model::ModelRow>, AuthzError> {
102 let pool = self.pool.clone();
103 run(async move { pool.query_rows(&sql, ¶ms).await })?.map_err(store_error)
104 }
105
106 fn placeholder(&self, n: usize) -> String {
108 if is_postgres(self.pool.backend()) { format!("${n}") } else { "?".to_string() }
109 }
110
111 fn forget(&self) {
112 self.cache.lock().unwrap_or_else(|e| e.into_inner()).clear();
113 }
114}
115
116fn is_mysql(backend: Backend) -> bool {
117 #[cfg(feature = "model-mysql")]
118 if matches!(backend, Backend::MySql) {
119 return true;
120 }
121 let _ = backend;
122 false
123}
124
125fn is_postgres(backend: Backend) -> bool {
126 #[cfg(feature = "model-postgres")]
127 if matches!(backend, Backend::Postgres) {
128 return true;
129 }
130 let _ = backend;
131 false
132}
133
134fn key(tuple: &Tuple) -> String {
135 crate::core::sha256::sha256(tuple.to_string().as_bytes()).iter().map(|b| format!("{b:02x}")).collect()
136}
137
138fn subject_from(row: &crate::model::ModelRow) -> Result<Subject, AuthzError> {
139 let kind: String = row.get("subject_type").map_err(store_error)?;
140 let id: String = row.get("subject_id").map_err(store_error)?;
141 let relation: String = row.get("subject_relation").map_err(store_error)?;
142 Ok(Subject { object: ObjectRef { kind, id }, relation: (!relation.is_empty()).then_some(relation) })
143}
144
145impl TupleStore for SqlStore {
146 fn write(&self, tuple: &Tuple) -> Result<bool, AuthzError> {
147 let backend = self.pool.backend();
148 let values: Vec<String> = (1..=7).map(|n| self.placeholder(n)).collect();
149 let columns = "tuple_key, object_type, object_id, relation, subject_type, subject_id, subject_relation";
150 let sql = if is_mysql(backend) {
151 format!("INSERT IGNORE INTO {TABLE} ({columns}) VALUES ({})", values.join(", "))
152 } else if is_postgres(backend) {
153 format!("INSERT INTO {TABLE} ({columns}) VALUES ({}) ON CONFLICT DO NOTHING", values.join(", "))
154 } else {
155 format!("INSERT OR IGNORE INTO {TABLE} ({columns}) VALUES ({})", values.join(", "))
156 };
157 let params = vec![
158 Value::Text(key(tuple)),
159 Value::Text(tuple.object.kind.clone()),
160 Value::Text(tuple.object.id.clone()),
161 Value::Text(tuple.relation.clone()),
162 Value::Text(tuple.subject.object.kind.clone()),
163 Value::Text(tuple.subject.object.id.clone()),
164 Value::Text(tuple.subject.relation.clone().unwrap_or_default()),
165 ];
166 let inserted = self.execute(sql, params)? > 0;
167 self.forget();
168 Ok(inserted)
169 }
170
171 fn delete(&self, tuple: &Tuple) -> Result<bool, AuthzError> {
172 let sql = format!("DELETE FROM {TABLE} WHERE tuple_key = {}", self.placeholder(1));
173 let removed = self.execute(sql, vec![Value::Text(key(tuple))])? > 0;
174 self.forget();
175 Ok(removed)
176 }
177
178 fn read(&self, object: &ObjectRef, relation: &str) -> Result<Vec<Subject>, AuthzError> {
179 let cache_key = (object.clone(), relation.to_string());
180 if let Some((at, subjects)) = self.cache.lock().unwrap_or_else(|e| e.into_inner()).get(&cache_key) {
181 if at.elapsed() < READ_CACHE {
182 return Ok(subjects.clone());
183 }
184 }
185 let sql = format!(
186 "SELECT subject_type, subject_id, subject_relation FROM {TABLE} WHERE object_type = {} AND object_id = {} AND relation = {}",
187 self.placeholder(1),
188 self.placeholder(2),
189 self.placeholder(3)
190 );
191 let rows = self.query(sql, vec![Value::Text(object.kind.clone()), Value::Text(object.id.clone()), Value::Text(relation.to_string())])?;
192 let subjects = rows.iter().map(subject_from).collect::<Result<Vec<_>, _>>()?;
193 let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
194 if cache.len() > 10_000 {
195 cache.clear();
196 }
197 cache.insert(cache_key, (Instant::now(), subjects.clone()));
198 Ok(subjects)
199 }
200
201 fn list(&self, filter: &TupleFilter, limit: usize) -> Result<Vec<Tuple>, AuthzError> {
202 let mut conditions = Vec::new();
203 let mut params = Vec::new();
204 let mut add = |column: &str, value: String, conditions: &mut Vec<String>| {
205 params.push(Value::Text(value));
206 conditions.push(format!("{column} = {}", self.placeholder(params.len())));
207 };
208 if let Some(object) = &filter.object {
209 add("object_type", object.kind.clone(), &mut conditions);
210 add("object_id", object.id.clone(), &mut conditions);
211 }
212 if let Some(kind) = &filter.object_kind {
213 add("object_type", kind.clone(), &mut conditions);
214 }
215 if let Some(relation) = &filter.relation {
216 add("relation", relation.clone(), &mut conditions);
217 }
218 if let Some(subject) = &filter.subject {
219 add("subject_type", subject.object.kind.clone(), &mut conditions);
220 add("subject_id", subject.object.id.clone(), &mut conditions);
221 add("subject_relation", subject.relation.clone().unwrap_or_default(), &mut conditions);
222 }
223 let filter_sql = if conditions.is_empty() { String::new() } else { format!(" WHERE {}", conditions.join(" AND ")) };
224 let sql = format!(
225 "SELECT object_type, object_id, relation, subject_type, subject_id, subject_relation FROM {TABLE}{filter_sql} \
226 ORDER BY object_type, object_id, relation, subject_type, subject_id, subject_relation LIMIT {}",
227 limit.min(100_000)
228 );
229 let rows = self.query(sql, params)?;
230 rows.iter()
231 .map(|row| {
232 Ok(Tuple {
233 object: ObjectRef { kind: row.get("object_type").map_err(store_error)?, id: row.get("object_id").map_err(store_error)? },
234 relation: row.get("relation").map_err(store_error)?,
235 subject: subject_from(row)?,
236 })
237 })
238 .collect()
239 }
240}
241
242#[doc(hidden)]
245pub fn config_store() -> Result<Arc<dyn TupleStore>, String> {
246 static STORE: OnceLock<Result<Arc<SqlStore>, String>> = OnceLock::new();
247 STORE
248 .get_or_init(|| SqlStore::from_env().map(Arc::new).map_err(|e| e.to_string()))
249 .clone()
250 .map(|store| store as Arc<dyn TupleStore>)
251}
252
253#[cfg(test)]
254mod tests;