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	///
174	/// The `roles` parameter should be the authenticated user's roles.
175	/// `PermissionLevel::User` means owner/leader of the target tenant, not "any
176	/// authenticated user" — see [`super::types::PermissionLevel::check`].
177	pub async fn set<S: AsRef<str>>(
178		&self,
179		tn_id: TnId,
180		key: &str,
181		value: SettingValue,
182		roles: &[S],
183	) -> ClResult<Setting> {
184		// Get definition (supports wildcard patterns like "ui.*")
185		let def = self.definition(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: the tenant's own row — `def.permission` above already
227				// established the caller may configure this tenant.
228				tn_id
229			}
230		};
231
232		self.write_at(def, storage_tn_id, key, value).await
233	}
234
235	/// Set a tenant's setting on the system's own behalf, bypassing the permission
236	/// check. The `set` half of [`Self::clear_system`], for server-initiated writes
237	/// with no authenticated principal: seeding `ui.onboarding` on a just-created
238	/// tenant, or storing the `profile.lang` a registration form supplied before
239	/// anyone can log in.
240	///
241	/// Same narrow shape as `clear_system`: only a Tenant-scoped key on a real tenant,
242	/// so the shared global default row stays SADM-only through [`Self::set`]. Type
243	/// validation and the custom validator still run.
244	pub async fn set_system(
245		&self,
246		tn_id: TnId,
247		key: &str,
248		value: SettingValue,
249	) -> ClResult<Setting> {
250		let def = self.definition(key)?;
251
252		if def.scope != SettingScope::Tenant || tn_id.0 == 0 {
253			warn!(
254				"System write refused for setting '{}' (scope {:?}, tn_id={})",
255				key, def.scope, tn_id.0
256			);
257			return Err(Error::PermissionDenied);
258		}
259
260		self.write_at(def, tn_id, key, value).await
261	}
262
263	/// Validate and store one resolved `(tn_id, key)` row. Shared tail of
264	/// [`Self::set`] and [`Self::set_system`]; the caller has already decided
265	/// that the write is permitted and which row it lands in.
266	async fn write_at(
267		&self,
268		def: &SettingDefinition,
269		storage_tn_id: TnId,
270		key: &str,
271		value: SettingValue,
272	) -> ClResult<Setting> {
273		// Validate type matches definition (if default exists)
274		if let Some(default) = &def.default
275			&& !value.matches_type(default)
276		{
277			return Err(Error::ValidationError(format!(
278				"Type mismatch for setting '{}': expected {}, got {}",
279				key,
280				default.type_name(),
281				value.type_name()
282			)));
283		}
284
285		// Run custom validator if present
286		if let Some(validator) = &def.validator {
287			validator(&value)?;
288		}
289
290		// Convert to JSON and save to database
291		let json_value = serde_json::to_value(&value)
292			.map_err(|e| Error::ValidationError(format!("Failed to serialize setting: {}", e)))?;
293		self.meta.update_setting(storage_tn_id, key, Some(json_value)).await?;
294
295		// Invalidate cached entries for this key (across all tenants), so
296		// any tenant whose value resolved through the now-stale (tenant or
297		// global) row re-resolves on next read.
298		self.cache.invalidate_key(key);
299
300		info!("Setting '{}' updated for tn_id={}", key, storage_tn_id.0);
301
302		// Return the setting (note: the current adapter doesn't track updated_at, so we use now)
303		Ok(Setting {
304			key: key.to_string(),
305			value,
306			tn_id: storage_tn_id,
307			updated_at: cloudillo_types::types::Timestamp::now(),
308		})
309	}
310
311	/// Delete a setting (falls back to next level)
312	pub async fn delete(&self, tn_id: TnId, key: &str) -> ClResult<bool> {
313		self.meta.update_setting(tn_id, key, None).await?;
314		self.cache.invalidate_key(key);
315
316		info!("Setting '{}' deleted for tn_id={}", key, tn_id.0);
317		Ok(true)
318	}
319
320	/// Clear (unset) a setting with the same role-gating and scope checks as
321	/// `set`. Use this instead of calling `MetaAdapter::update_setting(..., None)`
322	/// directly when the caller is acting on behalf of an authenticated user —
323	/// it keeps audit trails and permission checks consistent across set/clear.
324	pub async fn clear<S: AsRef<str>>(&self, tn_id: TnId, key: &str, roles: &[S]) -> ClResult<()> {
325		let def = self.definition(key)?;
326
327		if !def.permission.check(roles) {
328			warn!(
329				"Permission denied for clearing setting '{}': requires {:?}",
330				key, def.permission
331			);
332			return Err(Error::PermissionDenied);
333		}
334
335		// Same invariant as `set`: clearing the (Tenant|Global, TnId(0)) row
336		// touches the shared global default that every tenant resolves
337		// through, and the HTTP `delete_setting` handler already requires
338		// SADM unconditionally for `level=global`. Caller `tn_id == 0`
339		// reaches here only via SADM in practice (auth.tn_id==0 only for the
340		// system tenant; cross-tenant `tenant=` resolves to `TnId(0)` only
341		// when caller is SADM).
342		let storage_tn_id = match (def.scope, tn_id.0) {
343			(SettingScope::System, _) => return Err(Error::PermissionDenied),
344			(SettingScope::Global | SettingScope::Tenant, 0) => {
345				// Caller-supplied tn_id==0 targets the shared global default
346				// row that every tenant resolves through — gate symmetrically
347				// with `set` to keep non-HTTP callers honest.
348				if !roles.iter().any(|r| r.as_ref() == "SADM") {
349					return Err(Error::PermissionDenied);
350				}
351				TnId(0)
352			}
353			(SettingScope::Global, _) => {
354				if !roles.iter().any(|r| r.as_ref() == "SADM") {
355					return Err(Error::PermissionDenied);
356				}
357				TnId(0)
358			}
359			(SettingScope::Tenant, _) => tn_id,
360		};
361
362		self.clear_at(storage_tn_id, key).await
363	}
364
365	/// Clear a tenant's setting on the system's own behalf, bypassing the permission
366	/// check. For server-initiated cleanup with no authenticated principal — see
367	/// `apply_onboarding_clear` in `cloudillo-profile`, which clears the onboarding
368	/// gate from the unauthenticated ref-scoped IDP-status handler.
369	///
370	/// Narrow on purpose: only a Tenant-scoped key on a real tenant. `System` scope is
371	/// never writable, and `tn_id == 0` (or a Global-scoped key, which always stores
372	/// there) is the shared default row every tenant resolves through — that stays
373	/// SADM-only through [`Self::clear`].
374	pub async fn clear_system(&self, tn_id: TnId, key: &str) -> ClResult<()> {
375		let def = self.definition(key)?;
376
377		if def.scope != SettingScope::Tenant || tn_id.0 == 0 {
378			warn!(
379				"System clear refused for setting '{}' (scope {:?}, tn_id={})",
380				key, def.scope, tn_id.0
381			);
382			return Err(Error::PermissionDenied);
383		}
384
385		self.clear_at(tn_id, key).await
386	}
387
388	/// Look up a definition, supporting wildcard patterns like `ui.*`.
389	fn definition(&self, key: &str) -> ClResult<&SettingDefinition> {
390		self.registry
391			.get(key)
392			.ok_or_else(|| Error::ValidationError(format!("Unknown setting: {}", key)))
393	}
394
395	/// Drop the stored row and the cached resolutions that flowed through it.
396	async fn clear_at(&self, storage_tn_id: TnId, key: &str) -> ClResult<()> {
397		self.meta.update_setting(storage_tn_id, key, None).await?;
398
399		// Invalidate this key across all tenants — even when clearing a
400		// per-tenant override, any other tenant whose cached resolution
401		// flowed through the same `key` should still re-resolve on next read.
402		self.cache.invalidate_key(key);
403
404		info!("Setting '{}' cleared for tn_id={}", key, storage_tn_id.0);
405		Ok(())
406	}
407
408	/// Validate that all required settings (no default and not optional) are configured
409	pub async fn validate_required_settings(&self) -> ClResult<()> {
410		for def in self.registry.list() {
411			// Skip optional settings and settings with defaults
412			if def.optional || def.default.is_some() {
413				continue;
414			}
415
416			// This setting is required - check if it's configured globally
417			if self.meta.read_setting(TnId(0), &def.key).await?.is_none() {
418				return Err(Error::ValidationError(format!(
419					"Required setting '{}' is not configured",
420					def.key
421				)));
422			}
423		}
424		Ok(())
425	}
426
427	/// Type-safe getters (required - returns error if not found)
428	pub async fn get_string(&self, tn_id: TnId, key: &str) -> ClResult<String> {
429		match self.get(tn_id, key).await? {
430			Some(SettingValue::String(s)) => Ok(s),
431			Some(v) => Err(Error::ValidationError(format!(
432				"Setting '{}' is not a string, got {}",
433				key,
434				v.type_name()
435			))),
436			None => Err(Error::SettingNotFound(format!(
437				"Setting '{}' has no default and must be configured",
438				key
439			))),
440		}
441	}
442
443	pub async fn get_int(&self, tn_id: TnId, key: &str) -> ClResult<i64> {
444		match self.get(tn_id, key).await? {
445			Some(SettingValue::Int(i)) => Ok(i),
446			Some(v) => Err(Error::ValidationError(format!(
447				"Setting '{}' is not an integer, got {}",
448				key,
449				v.type_name()
450			))),
451			None => Err(Error::SettingNotFound(format!(
452				"Setting '{}' has no default and must be configured",
453				key
454			))),
455		}
456	}
457
458	pub async fn get_bool(&self, tn_id: TnId, key: &str) -> ClResult<bool> {
459		match self.get(tn_id, key).await? {
460			Some(SettingValue::Bool(b)) => Ok(b),
461			Some(v) => Err(Error::ValidationError(format!(
462				"Setting '{}' is not a boolean, got {}",
463				key,
464				v.type_name()
465			))),
466			None => Err(Error::SettingNotFound(format!(
467				"Setting '{}' has no default and must be configured",
468				key
469			))),
470		}
471	}
472
473	pub async fn get_json(&self, tn_id: TnId, key: &str) -> ClResult<serde_json::Value> {
474		match self.get(tn_id, key).await? {
475			Some(SettingValue::Json(j)) => Ok(j),
476			Some(v) => Err(Error::ValidationError(format!(
477				"Setting '{}' is not JSON, got {}",
478				key,
479				v.type_name()
480			))),
481			None => Err(Error::SettingNotFound(format!(
482				"Setting '{}' has no default and must be configured",
483				key
484			))),
485		}
486	}
487
488	/// Type-safe optional getters (returns None if not found or has no default)
489	/// Still returns error if setting exists but has wrong type
490	pub async fn get_string_opt(&self, tn_id: TnId, key: &str) -> ClResult<Option<String>> {
491		match self.get(tn_id, key).await {
492			Ok(Some(SettingValue::String(s))) => Ok(Some(s)),
493			Ok(Some(v)) => Err(Error::ValidationError(format!(
494				"Setting '{}' is not a string, got {}",
495				key,
496				v.type_name()
497			))),
498			Ok(None) | Err(Error::SettingNotFound(_)) => Ok(None),
499			Err(e) => Err(e),
500		}
501	}
502
503	pub async fn get_int_opt(&self, tn_id: TnId, key: &str) -> ClResult<Option<i64>> {
504		match self.get(tn_id, key).await {
505			Ok(Some(SettingValue::Int(i))) => Ok(Some(i)),
506			Ok(Some(v)) => Err(Error::ValidationError(format!(
507				"Setting '{}' is not an integer, got {}",
508				key,
509				v.type_name()
510			))),
511			Ok(None) | Err(Error::SettingNotFound(_)) => Ok(None),
512			Err(e) => Err(e),
513		}
514	}
515
516	pub async fn get_bool_opt(&self, tn_id: TnId, key: &str) -> ClResult<Option<bool>> {
517		match self.get(tn_id, key).await {
518			Ok(Some(SettingValue::Bool(b))) => Ok(Some(b)),
519			Ok(Some(v)) => Err(Error::ValidationError(format!(
520				"Setting '{}' is not a boolean, got {}",
521				key,
522				v.type_name()
523			))),
524			Ok(None) | Err(Error::SettingNotFound(_)) => Ok(None),
525			Err(e) => Err(e),
526		}
527	}
528
529	pub async fn get_json_opt(
530		&self,
531		tn_id: TnId,
532		key: &str,
533	) -> ClResult<Option<serde_json::Value>> {
534		match self.get(tn_id, key).await {
535			Ok(Some(SettingValue::Json(j))) => Ok(Some(j)),
536			Ok(Some(v)) => Err(Error::ValidationError(format!(
537				"Setting '{}' is not JSON, got {}",
538				key,
539				v.type_name()
540			))),
541			Ok(None) | Err(Error::SettingNotFound(_)) => Ok(None),
542			Err(e) => Err(e),
543		}
544	}
545
546	/// Get reference to registry (for listing all settings)
547	pub fn registry(&self) -> &Arc<FrozenSettingsRegistry> {
548		&self.registry
549	}
550
551	/// List stored settings by prefix with definition metadata
552	///
553	/// This queries the database for actual stored settings matching the prefixes,
554	/// then resolves each against the registry (supporting wildcard patterns like "ui.*").
555	/// Global settings are merged with tenant-specific settings (tenant overrides global).
556	pub async fn list_by_prefix(
557		&self,
558		tn_id: TnId,
559		prefixes: &[String],
560	) -> ClResult<Vec<(String, SettingValue, &SettingDefinition)>> {
561		let prefixes_dotted: Vec<String> = prefixes.iter().map(|p| format!("{}.", p)).collect();
562
563		// Get global settings first (tn_id=0)
564		let global_settings = self.meta.list_settings(TnId(0), Some(&prefixes_dotted)).await?;
565
566		// Get tenant-specific settings (override global)
567		let tenant_settings = if tn_id.0 != 0 {
568			self.meta.list_settings(tn_id, Some(&prefixes_dotted)).await?
569		} else {
570			std::collections::HashMap::new()
571		};
572
573		// Merge: tenant overrides global
574		let mut merged = global_settings;
575		merged.extend(tenant_settings);
576
577		let mut result = Vec::new();
578		for (key, json_value) in merged {
579			if let Some(definition) = self.registry.get(&key) {
580				let value = serde_json::from_value::<SettingValue>(json_value)
581					.map_err(|e| Error::ValidationError(format!("Invalid setting value: {}", e)))?;
582				result.push((key, value, definition));
583			}
584		}
585
586		Ok(result)
587	}
588
589	/// List stored settings at exactly one level (no merge, no fallback).
590	/// Used by the list handler when an explicit `level=` is requested.
591	pub async fn list_by_prefix_at(
592		&self,
593		tn_id: TnId,
594		prefixes: &[String],
595	) -> ClResult<Vec<(String, SettingValue, &SettingDefinition)>> {
596		let prefixes_dotted: Vec<String> = prefixes.iter().map(|p| format!("{}.", p)).collect();
597		let rows = self.meta.list_settings(tn_id, Some(&prefixes_dotted)).await?;
598		let mut result = Vec::new();
599		for (key, json_value) in rows {
600			if let Some(definition) = self.registry.get(&key) {
601				let value = serde_json::from_value::<SettingValue>(json_value)
602					.map_err(|e| Error::ValidationError(format!("Invalid setting value: {}", e)))?;
603				result.push((key, value, definition));
604			}
605		}
606		Ok(result)
607	}
608}
609
610// vim: ts=4