Skip to main content

cloudillo_core/settings/
service.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! Settings service with caching, validation, and permission checks
5
6use lru::LruCache;
7use std::num::NonZeroUsize;
8use std::sync::Arc;
9
10use crate::prelude::*;
11use cloudillo_types::meta_adapter::MetaAdapter;
12
13use super::types::{
14	DefinitionMatch, FrozenSettingsRegistry, Setting, SettingDefinition, SettingScope, SettingValue,
15};
16
17// Compile-time constant for default cache capacity
18const DEFAULT_CACHE_CAPACITY: NonZeroUsize = match NonZeroUsize::new(100) {
19	Some(n) => n,
20	None => unreachable!(),
21};
22
23/// LRU cache for settings values.
24/// Uses Mutex because LruCache::get mutates internal recency state.
25pub struct SettingsCache {
26	cache: Arc<parking_lot::Mutex<LruCache<(TnId, String), SettingValue>>>,
27}
28
29impl SettingsCache {
30	pub fn new(capacity: usize) -> Self {
31		let non_zero = NonZeroUsize::new(capacity).unwrap_or(DEFAULT_CACHE_CAPACITY);
32		Self { cache: Arc::new(parking_lot::Mutex::new(LruCache::new(non_zero))) }
33	}
34
35	pub fn get(&self, tn_id: TnId, key: &str) -> Option<SettingValue> {
36		let mut cache = self.cache.lock();
37		cache.get(&(tn_id, key.to_string())).cloned()
38	}
39
40	pub fn put(&self, tn_id: TnId, key: String, value: SettingValue) {
41		let mut cache = self.cache.lock();
42		cache.put((tn_id, key), value);
43	}
44
45	/// Invalidate all cached settings
46	pub fn clear(&self) {
47		let mut cache = self.cache.lock();
48		cache.clear();
49	}
50
51	/// Invalidate cached entries for a specific key across all tenants
52	/// (typically called after a global setting changes, so each tenant
53	/// re-resolves through the new global default on next read).
54	pub fn invalidate_key(&self, key: &str) {
55		let mut cache = self.cache.lock();
56		// `LruCache` has no "remove by predicate" API, so collect matching
57		// composite keys first and pop them in a second pass — bounded by the
58		// cache capacity (default 100), so this is cheap.
59		let to_remove: Vec<(TnId, String)> =
60			cache.iter().filter(|((_, k), _)| k == key).map(|(k, _)| k.clone()).collect();
61		for k in to_remove {
62			cache.pop(&k);
63		}
64	}
65}
66
67/// Settings service - main interface for accessing and managing settings
68pub struct SettingsService {
69	registry: Arc<FrozenSettingsRegistry>,
70	cache: SettingsCache,
71	meta: Arc<dyn MetaAdapter>,
72}
73
74impl SettingsService {
75	pub fn new(
76		registry: Arc<FrozenSettingsRegistry>,
77		meta: Arc<dyn MetaAdapter>,
78		cache_size: usize,
79	) -> Self {
80		Self { registry, cache: SettingsCache::new(cache_size), meta }
81	}
82
83	/// Get setting value with full resolution (tenant -> global -> default).
84	///
85	/// Three distinct outcomes:
86	/// - `Ok(Some(value))` — value resolved (stored or default)
87	/// - `Ok(None)` — wildcard-namespace key with no stored value (legitimate
88	///   absence; wildcard registrations declare a namespace, not fixed keys)
89	/// - `Err(SettingNotFound)` — exact-match key with no default and not
90	///   configured (programmer/configuration error)
91	/// - `Err(other)` — transient adapter or deserialization error
92	pub async fn get(&self, tn_id: TnId, key: &str) -> ClResult<Option<SettingValue>> {
93		// Check cache (tenant-specific first, then global fallback)
94		if let Some(value) = self.cache.get(tn_id, key) {
95			debug!("Setting cache hit: {}.{}", tn_id.0, key);
96			return Ok(Some(value));
97		}
98		if tn_id.0 != 0
99			&& let Some(value) = self.cache.get(TnId(0), key)
100		{
101			debug!("Setting cache hit (global fallback): {}", key);
102			return Ok(Some(value));
103		}
104
105		// Get definition (supports wildcard patterns like "ui.*")
106		let m = self
107			.registry
108			.get_match(key)
109			.ok_or_else(|| Error::SettingNotFound(format!("Unknown setting: {}", key)))?;
110
111		// Try tenant-specific setting
112		if tn_id.0 != 0
113			&& let Some(json_value) = self.meta.read_setting(tn_id, key).await?
114		{
115			let value = serde_json::from_value::<SettingValue>(json_value)
116				.map_err(|e| Error::ValidationError(format!("Invalid setting value: {}", e)))?;
117			self.cache.put(tn_id, key.to_string(), value.clone());
118			return Ok(Some(value));
119		}
120
121		// Try global setting — cache under TnId(0) so tenant overrides aren't masked
122		if let Some(json_value) = self.meta.read_setting(TnId(0), key).await? {
123			let value = serde_json::from_value::<SettingValue>(json_value)
124				.map_err(|e| Error::ValidationError(format!("Invalid setting value: {}", e)))?;
125			self.cache.put(TnId(0), key.to_string(), value.clone());
126			return Ok(Some(value));
127		}
128
129		let def = match m {
130			DefinitionMatch::Exact(d) => d,
131			DefinitionMatch::Wildcard(_) => return Ok(None),
132		};
133		match &def.default {
134			Some(default) => {
135				let value = default.clone();
136				self.cache.put(tn_id, key.to_string(), value.clone());
137				Ok(Some(value))
138			}
139			None => Err(Error::SettingNotFound(format!(
140				"Setting '{}' has no default and must be configured",
141				key
142			))),
143		}
144	}
145
146	/// Get the raw stored value at a single level without fallback.
147	///
148	/// Unlike `get`, this does not consult the schema default or the global
149	/// row when querying a tenant — it only returns the value stored in the
150	/// row keyed by `(tn_id, key)`. Useful for the UI to distinguish "no
151	/// per-tenant override" from "explicit override that happens to equal
152	/// the global value".
153	///
154	/// Returns `Ok(None)` when no row exists at that level. Bypasses cache
155	/// because the cache stores resolved values, not raw rows.
156	pub async fn get_raw(&self, tn_id: TnId, key: &str) -> ClResult<Option<SettingValue>> {
157		// Validate the key is registered (matches the strictness of `get`).
158		self.registry
159			.get_match(key)
160			.ok_or_else(|| Error::SettingNotFound(format!("Unknown setting: {}", key)))?;
161
162		match self.meta.read_setting(tn_id, key).await? {
163			Some(json_value) => {
164				let value = serde_json::from_value::<SettingValue>(json_value)
165					.map_err(|e| Error::ValidationError(format!("Invalid setting value: {}", e)))?;
166				Ok(Some(value))
167			}
168			None => Ok(None),
169		}
170	}
171
172	/// Set setting value with validation and permission checks
173	/// The `roles` parameter should be the authenticated user's roles
174	pub async fn set<S: AsRef<str>>(
175		&self,
176		tn_id: TnId,
177		key: &str,
178		value: SettingValue,
179		roles: &[S],
180	) -> ClResult<Setting> {
181		// Get definition (supports wildcard patterns like "ui.*")
182		let def = self
183			.registry
184			.get(key)
185			.ok_or_else(|| Error::ValidationError(format!("Unknown setting: {}", key)))?;
186
187		// Check permission level
188		if !def.permission.check(roles) {
189			warn!("Permission denied for setting '{}': requires {:?}", key, def.permission);
190			return Err(Error::PermissionDenied);
191		}
192
193		// Check scope validity
194		// Determine the actual tn_id to use for storage.
195		//
196		// (Tenant, 0) writes the shared global default row that every tenant
197		// resolves through, so it is SADM-only — same invariant `clear`
198		// enforces below. The HTTP path reaches this arm only via SADM (since
199		// `resolve_target_tn_id` already gates cross-tenant access), but
200		// non-HTTP callers (`community.rs` etc.) come straight in and would
201		// otherwise be a privilege-escalation footgun.
202		let storage_tn_id = match (def.scope, tn_id.0) {
203			(SettingScope::System, _) => {
204				return Err(Error::PermissionDenied);
205			}
206			(SettingScope::Global | SettingScope::Tenant, 0) => {
207				// Writing the global default row affects every tenant —
208				// require SADM regardless of scope. Today the HTTP handler
209				// passes `acting_tn_id` (never 0 for non-SADM), so this is
210				// defense-in-depth against non-HTTP callers and consistency
211				// with the `clear` invariant below.
212				if !roles.iter().any(|r| r.as_ref() == "SADM") {
213					return Err(Error::PermissionDenied);
214				}
215				TnId(0)
216			}
217			(SettingScope::Global, _) => {
218				// Admin users can update global settings from their tenant context
219				// The setting is stored with tn_id=0 to be global
220				if !roles.iter().any(|r| r.as_ref() == "SADM") {
221					return Err(Error::PermissionDenied);
222				}
223				TnId(0)
224			}
225			(SettingScope::Tenant, _) => {
226				// OK: Setting tenant-specific value
227				tn_id
228			}
229		};
230
231		// Validate type matches definition (if default exists)
232		if let Some(default) = &def.default
233			&& !value.matches_type(default)
234		{
235			return Err(Error::ValidationError(format!(
236				"Type mismatch for setting '{}': expected {}, got {}",
237				key,
238				default.type_name(),
239				value.type_name()
240			)));
241		}
242
243		// Run custom validator if present
244		if let Some(validator) = &def.validator {
245			validator(&value)?;
246		}
247
248		// Convert to JSON and save to database
249		let json_value = serde_json::to_value(&value)
250			.map_err(|e| Error::ValidationError(format!("Failed to serialize setting: {}", e)))?;
251		self.meta.update_setting(storage_tn_id, key, Some(json_value)).await?;
252
253		// Invalidate cached entries for this key (across all tenants), so
254		// any tenant whose value resolved through the now-stale (tenant or
255		// global) row re-resolves on next read.
256		self.cache.invalidate_key(key);
257
258		info!("Setting '{}' updated for tn_id={}", key, storage_tn_id.0);
259
260		// Return the setting (note: the current adapter doesn't track updated_at, so we use now)
261		Ok(Setting {
262			key: key.to_string(),
263			value,
264			tn_id: storage_tn_id,
265			updated_at: cloudillo_types::types::Timestamp::now(),
266		})
267	}
268
269	/// Delete a setting (falls back to next level)
270	pub async fn delete(&self, tn_id: TnId, key: &str) -> ClResult<bool> {
271		self.meta.update_setting(tn_id, key, None).await?;
272		self.cache.invalidate_key(key);
273
274		info!("Setting '{}' deleted for tn_id={}", key, tn_id.0);
275		Ok(true)
276	}
277
278	/// Clear (unset) a setting with the same role-gating and scope checks as
279	/// `set`. Use this instead of calling `MetaAdapter::update_setting(..., None)`
280	/// directly when the caller is acting on behalf of an authenticated user —
281	/// it keeps audit trails and permission checks consistent across set/clear.
282	pub async fn clear<S: AsRef<str>>(&self, tn_id: TnId, key: &str, roles: &[S]) -> ClResult<()> {
283		let def = self
284			.registry
285			.get(key)
286			.ok_or_else(|| Error::ValidationError(format!("Unknown setting: {}", key)))?;
287
288		if !def.permission.check(roles) {
289			warn!(
290				"Permission denied for clearing setting '{}': requires {:?}",
291				key, def.permission
292			);
293			return Err(Error::PermissionDenied);
294		}
295
296		// Same invariant as `set`: clearing the (Tenant|Global, TnId(0)) row
297		// touches the shared global default that every tenant resolves
298		// through, and the HTTP `delete_setting` handler already requires
299		// SADM unconditionally for `level=global`. Caller `tn_id == 0`
300		// reaches here only via SADM in practice (auth.tn_id==0 only for the
301		// system tenant; cross-tenant `tenant=` resolves to `TnId(0)` only
302		// when caller is SADM).
303		let storage_tn_id = match (def.scope, tn_id.0) {
304			(SettingScope::System, _) => return Err(Error::PermissionDenied),
305			(SettingScope::Global | SettingScope::Tenant, 0) => {
306				// Caller-supplied tn_id==0 targets the shared global default
307				// row that every tenant resolves through — gate symmetrically
308				// with `set` to keep non-HTTP callers honest.
309				if !roles.iter().any(|r| r.as_ref() == "SADM") {
310					return Err(Error::PermissionDenied);
311				}
312				TnId(0)
313			}
314			(SettingScope::Global, _) => {
315				if !roles.iter().any(|r| r.as_ref() == "SADM") {
316					return Err(Error::PermissionDenied);
317				}
318				TnId(0)
319			}
320			(SettingScope::Tenant, _) => tn_id,
321		};
322
323		self.meta.update_setting(storage_tn_id, key, None).await?;
324
325		// Invalidate this key across all tenants — even when clearing a
326		// per-tenant override, any other tenant whose cached resolution
327		// flowed through the same `key` should still re-resolve on next read.
328		self.cache.invalidate_key(key);
329
330		info!("Setting '{}' cleared for tn_id={}", key, storage_tn_id.0);
331		Ok(())
332	}
333
334	/// Validate that all required settings (no default and not optional) are configured
335	pub async fn validate_required_settings(&self) -> ClResult<()> {
336		for def in self.registry.list() {
337			// Skip optional settings and settings with defaults
338			if def.optional || def.default.is_some() {
339				continue;
340			}
341
342			// This setting is required - check if it's configured globally
343			if self.meta.read_setting(TnId(0), &def.key).await?.is_none() {
344				return Err(Error::ValidationError(format!(
345					"Required setting '{}' is not configured",
346					def.key
347				)));
348			}
349		}
350		Ok(())
351	}
352
353	/// Type-safe getters (required - returns error if not found)
354	pub async fn get_string(&self, tn_id: TnId, key: &str) -> ClResult<String> {
355		match self.get(tn_id, key).await? {
356			Some(SettingValue::String(s)) => Ok(s),
357			Some(v) => Err(Error::ValidationError(format!(
358				"Setting '{}' is not a string, got {}",
359				key,
360				v.type_name()
361			))),
362			None => Err(Error::SettingNotFound(format!(
363				"Setting '{}' has no default and must be configured",
364				key
365			))),
366		}
367	}
368
369	pub async fn get_int(&self, tn_id: TnId, key: &str) -> ClResult<i64> {
370		match self.get(tn_id, key).await? {
371			Some(SettingValue::Int(i)) => Ok(i),
372			Some(v) => Err(Error::ValidationError(format!(
373				"Setting '{}' is not an integer, got {}",
374				key,
375				v.type_name()
376			))),
377			None => Err(Error::SettingNotFound(format!(
378				"Setting '{}' has no default and must be configured",
379				key
380			))),
381		}
382	}
383
384	pub async fn get_bool(&self, tn_id: TnId, key: &str) -> ClResult<bool> {
385		match self.get(tn_id, key).await? {
386			Some(SettingValue::Bool(b)) => Ok(b),
387			Some(v) => Err(Error::ValidationError(format!(
388				"Setting '{}' is not a boolean, got {}",
389				key,
390				v.type_name()
391			))),
392			None => Err(Error::SettingNotFound(format!(
393				"Setting '{}' has no default and must be configured",
394				key
395			))),
396		}
397	}
398
399	pub async fn get_json(&self, tn_id: TnId, key: &str) -> ClResult<serde_json::Value> {
400		match self.get(tn_id, key).await? {
401			Some(SettingValue::Json(j)) => Ok(j),
402			Some(v) => Err(Error::ValidationError(format!(
403				"Setting '{}' is not JSON, got {}",
404				key,
405				v.type_name()
406			))),
407			None => Err(Error::SettingNotFound(format!(
408				"Setting '{}' has no default and must be configured",
409				key
410			))),
411		}
412	}
413
414	/// Type-safe optional getters (returns None if not found or has no default)
415	/// Still returns error if setting exists but has wrong type
416	pub async fn get_string_opt(&self, tn_id: TnId, key: &str) -> ClResult<Option<String>> {
417		match self.get(tn_id, key).await {
418			Ok(Some(SettingValue::String(s))) => Ok(Some(s)),
419			Ok(Some(v)) => Err(Error::ValidationError(format!(
420				"Setting '{}' is not a string, got {}",
421				key,
422				v.type_name()
423			))),
424			Ok(None) | Err(Error::SettingNotFound(_)) => Ok(None),
425			Err(e) => Err(e),
426		}
427	}
428
429	pub async fn get_int_opt(&self, tn_id: TnId, key: &str) -> ClResult<Option<i64>> {
430		match self.get(tn_id, key).await {
431			Ok(Some(SettingValue::Int(i))) => Ok(Some(i)),
432			Ok(Some(v)) => Err(Error::ValidationError(format!(
433				"Setting '{}' is not an integer, got {}",
434				key,
435				v.type_name()
436			))),
437			Ok(None) | Err(Error::SettingNotFound(_)) => Ok(None),
438			Err(e) => Err(e),
439		}
440	}
441
442	pub async fn get_bool_opt(&self, tn_id: TnId, key: &str) -> ClResult<Option<bool>> {
443		match self.get(tn_id, key).await {
444			Ok(Some(SettingValue::Bool(b))) => Ok(Some(b)),
445			Ok(Some(v)) => Err(Error::ValidationError(format!(
446				"Setting '{}' is not a boolean, got {}",
447				key,
448				v.type_name()
449			))),
450			Ok(None) | Err(Error::SettingNotFound(_)) => Ok(None),
451			Err(e) => Err(e),
452		}
453	}
454
455	pub async fn get_json_opt(
456		&self,
457		tn_id: TnId,
458		key: &str,
459	) -> ClResult<Option<serde_json::Value>> {
460		match self.get(tn_id, key).await {
461			Ok(Some(SettingValue::Json(j))) => Ok(Some(j)),
462			Ok(Some(v)) => Err(Error::ValidationError(format!(
463				"Setting '{}' is not JSON, got {}",
464				key,
465				v.type_name()
466			))),
467			Ok(None) | Err(Error::SettingNotFound(_)) => Ok(None),
468			Err(e) => Err(e),
469		}
470	}
471
472	/// Get reference to registry (for listing all settings)
473	pub fn registry(&self) -> &Arc<FrozenSettingsRegistry> {
474		&self.registry
475	}
476
477	/// List stored settings by prefix with definition metadata
478	///
479	/// This queries the database for actual stored settings matching the prefix,
480	/// then resolves each against the registry (supporting wildcard patterns like "ui.*").
481	/// Global settings are merged with tenant-specific settings (tenant overrides global).
482	pub async fn list_by_prefix(
483		&self,
484		tn_id: TnId,
485		prefix: &str,
486	) -> ClResult<Vec<(String, SettingValue, &SettingDefinition)>> {
487		let prefixes = vec![format!("{}.", prefix)]; // "ui" -> "ui."
488
489		// Get global settings first (tn_id=0)
490		let global_settings = self.meta.list_settings(TnId(0), Some(&prefixes)).await?;
491
492		// Get tenant-specific settings (override global)
493		let tenant_settings = if tn_id.0 != 0 {
494			self.meta.list_settings(tn_id, Some(&prefixes)).await?
495		} else {
496			std::collections::HashMap::new()
497		};
498
499		// Merge: tenant overrides global
500		let mut merged = global_settings;
501		merged.extend(tenant_settings);
502
503		let mut result = Vec::new();
504		for (key, json_value) in merged {
505			if let Some(definition) = self.registry.get(&key) {
506				let value = serde_json::from_value::<SettingValue>(json_value)
507					.map_err(|e| Error::ValidationError(format!("Invalid setting value: {}", e)))?;
508				result.push((key, value, definition));
509			}
510		}
511
512		Ok(result)
513	}
514}
515
516// vim: ts=4