1use platform_core::{
2 AppContext, RuntimeConfigDescriptor, RuntimeConfigScope, RuntimeConfigSnapshot,
3 RuntimeConfigType,
4};
5use std::collections::BTreeMap;
6use std::sync::LazyLock;
7
8const SESSION_CACHE_KEY: &str = "auth.session_cache";
9const CONSOLE_ADMIN_USER_SCOPES_KEY: &str = "auth.console_admin_user_scopes";
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum SessionCacheMode {
13 Database,
14 Redis,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct AuthRuntimeConfig {
19 pub session_cache: SessionCacheMode,
20 pub console_admin_user_scopes: BTreeMap<String, Vec<String>>,
21}
22
23impl AuthRuntimeConfig {
24 #[must_use]
25 pub fn from_context(ctx: &AppContext) -> Self {
26 Self::from_snapshot(&ctx.runtime_config.snapshot())
27 }
28
29 #[must_use]
30 pub fn from_snapshot(snapshot: &RuntimeConfigSnapshot) -> Self {
31 let session_cache = snapshot
32 .raw(SESSION_CACHE_KEY)
33 .and_then(serde_json::Value::as_str)
34 .and_then(SessionCacheMode::from_value)
35 .unwrap_or(SessionCacheMode::Database);
36 let console_admin_user_scopes = snapshot
37 .raw(CONSOLE_ADMIN_USER_SCOPES_KEY)
38 .and_then(|value| serde_json::from_value(value.clone()).ok())
39 .unwrap_or_default();
40
41 Self {
42 session_cache,
43 console_admin_user_scopes,
44 }
45 }
46}
47
48impl Default for AuthRuntimeConfig {
49 fn default() -> Self {
50 Self {
51 session_cache: SessionCacheMode::Database,
52 console_admin_user_scopes: BTreeMap::new(),
53 }
54 }
55}
56
57impl SessionCacheMode {
58 fn from_value(value: &str) -> Option<Self> {
59 match value {
60 "database" => Some(Self::Database),
61 "redis" => Some(Self::Redis),
62 _ => None,
63 }
64 }
65}
66
67pub static RUNTIME_CONFIG: LazyLock<Vec<RuntimeConfigDescriptor>> = LazyLock::new(|| {
68 vec![
69 RuntimeConfigDescriptor {
70 key: SESSION_CACHE_KEY.to_owned(),
71 scope: RuntimeConfigScope::Shared,
72 group: None,
73 section: None,
74 order: 10,
75 visible_when: None,
76 generated: None,
77 value_type: RuntimeConfigType::Enum(&["database", "redis"]),
78 default: serde_json::json!("database"),
79 editable: true,
80 restart_only: true,
81 description: "Session cache backend used by auth session resolution.",
82 },
83 RuntimeConfigDescriptor {
84 key: CONSOLE_ADMIN_USER_SCOPES_KEY.to_owned(),
85 scope: RuntimeConfigScope::Shared,
86 group: None,
87 section: None,
88 order: 20,
89 visible_when: None,
90 generated: None,
91 value_type: RuntimeConfigType::Json,
92 default: serde_json::json!({}),
93 editable: true,
94 restart_only: true,
95 description: "Map of auth user ids to Console admin scopes. Users must include `console.admin` to enter admin HTTP endpoints.",
96 },
97 ]
98});
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use platform_core::{RuntimeConfigRegistry, RuntimeConfigSnapshot};
104 use serde_json::json;
105
106 #[test]
107 fn defaults_to_database_cache_and_empty_console_admin_scopes() {
108 let registry = RuntimeConfigRegistry::try_new(RUNTIME_CONFIG.clone()).unwrap();
109 let snapshot = RuntimeConfigSnapshot::resolve(®istry, "api", &BTreeMap::new());
110
111 let config = AuthRuntimeConfig::from_snapshot(&snapshot);
112
113 assert_eq!(config.session_cache, SessionCacheMode::Database);
114 assert!(config.console_admin_user_scopes.is_empty());
115 }
116
117 #[test]
118 fn reads_console_admin_user_scopes_from_runtime_config() {
119 let registry = RuntimeConfigRegistry::try_new(RUNTIME_CONFIG.clone()).unwrap();
120 let mut stored = BTreeMap::new();
121 stored.insert(
122 ("*".to_owned(), CONSOLE_ADMIN_USER_SCOPES_KEY.to_owned()),
123 json!({
124 "usr_admin": ["console.admin", "auth.users.read"]
125 }),
126 );
127 let snapshot = RuntimeConfigSnapshot::resolve(®istry, "api", &stored);
128
129 let config = AuthRuntimeConfig::from_snapshot(&snapshot);
130
131 assert_eq!(
132 config.console_admin_user_scopes.get("usr_admin"),
133 Some(&vec![
134 "console.admin".to_owned(),
135 "auth.users.read".to_owned()
136 ])
137 );
138 }
139}