kellnr_settings/
oauth2.rs1use clap_serde_derive::ClapSerde;
2use serde::{Deserialize, Serialize};
3
4fn default_scopes() -> Vec<String> {
5 vec![
6 "openid".to_string(),
7 "profile".to_string(),
8 "email".to_string(),
9 ]
10}
11
12fn default_button_text() -> String {
13 "Login with SSO".to_string()
14}
15
16#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, ClapSerde)]
18#[serde(default)]
19pub struct OAuth2 {
20 #[default(false)]
22 #[arg(id = "oauth2-enabled", long = "oauth2-enabled")]
23 pub enabled: bool,
24
25 #[default(None)]
27 #[arg(id = "oauth2-issuer-url", long = "oauth2-issuer-url")]
28 pub issuer_url: Option<String>,
29
30 #[default(None)]
32 #[arg(id = "oauth2-client-id", long = "oauth2-client-id")]
33 pub client_id: Option<String>,
34
35 #[default(None)]
37 #[serde(skip_serializing)]
38 #[arg(id = "oauth2-client-secret", long = "oauth2-client-secret")]
39 pub client_secret: Option<String>,
40
41 #[default(default_scopes())]
43 #[arg(id = "oauth2-scopes", long = "oauth2-scopes")]
44 pub scopes: Vec<String>,
45
46 #[default(true)]
48 #[arg(
49 id = "oauth2-auto-provision-users",
50 long = "oauth2-auto-provision-users"
51 )]
52 pub auto_provision_users: bool,
53
54 #[default(None)]
56 #[arg(id = "oauth2-admin-group-claim", long = "oauth2-admin-group-claim")]
57 pub admin_group_claim: Option<String>,
58
59 #[default(None)]
61 #[arg(id = "oauth2-admin-group-value", long = "oauth2-admin-group-value")]
62 pub admin_group_value: Option<String>,
63
64 #[default(None)]
66 #[arg(
67 id = "oauth2-read-only-group-claim",
68 long = "oauth2-read-only-group-claim"
69 )]
70 pub read_only_group_claim: Option<String>,
71
72 #[default(None)]
74 #[arg(
75 id = "oauth2-read-only-group-value",
76 long = "oauth2-read-only-group-value"
77 )]
78 pub read_only_group_value: Option<String>,
79
80 #[default(default_button_text())]
82 #[arg(id = "oauth2-button-text", long = "oauth2-button-text")]
83 pub button_text: String,
84}
85
86impl OAuth2 {
87 pub fn validate(&self) -> Result<(), String> {
90 if !self.enabled {
91 return Ok(());
92 }
93
94 if self.issuer_url.is_none() {
95 return Err("OAuth2 is enabled but issuer_url is not set".to_string());
96 }
97
98 if self.client_id.is_none() {
99 return Err("OAuth2 is enabled but client_id is not set".to_string());
100 }
101
102 if self.client_secret.is_none() {
105 return Err("OAuth2 is enabled but client_secret is not set. \
106 Set it via KELLNR_OAUTH2__CLIENT_SECRET environment variable"
107 .to_string());
108 }
109
110 if self.scopes.is_empty() {
111 return Err("OAuth2 scopes cannot be empty".to_string());
112 }
113
114 if !self.scopes.contains(&"openid".to_string()) {
115 return Err("OAuth2 scopes must contain 'openid' for OIDC".to_string());
116 }
117
118 Ok(())
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_default_oauth2() {
128 let oauth2 = OAuth2::default();
129 assert!(!oauth2.enabled);
130 assert!(oauth2.issuer_url.is_none());
131 assert!(oauth2.client_id.is_none());
132 assert!(oauth2.client_secret.is_none());
133 assert_eq!(oauth2.scopes, vec!["openid", "profile", "email"]);
134 assert!(oauth2.auto_provision_users);
135 assert_eq!(oauth2.button_text, "Login with SSO");
136 }
137
138 #[test]
139 fn test_validate_disabled() {
140 let oauth2 = OAuth2::default();
141 assert!(oauth2.validate().is_ok());
142 }
143
144 #[test]
145 fn test_validate_enabled_missing_issuer() {
146 let oauth2 = OAuth2 {
147 enabled: true,
148 ..Default::default()
149 };
150 let result = oauth2.validate();
151 assert!(result.is_err());
152 assert!(result.unwrap_err().contains("issuer_url"));
153 }
154
155 #[test]
156 fn test_validate_enabled_missing_client_id() {
157 let oauth2 = OAuth2 {
158 enabled: true,
159 issuer_url: Some("https://example.com".to_string()),
160 ..Default::default()
161 };
162 let result = oauth2.validate();
163 assert!(result.is_err());
164 assert!(result.unwrap_err().contains("client_id"));
165 }
166
167 #[test]
168 fn test_validate_enabled_missing_client_secret() {
169 let oauth2 = OAuth2 {
170 enabled: true,
171 issuer_url: Some("https://example.com".to_string()),
172 client_id: Some("client-id".to_string()),
173 ..Default::default()
174 };
175 let result = oauth2.validate();
176 assert!(result.is_err());
177 assert!(result.unwrap_err().contains("client_secret"));
178 }
179
180 #[test]
181 fn test_validate_enabled_valid() {
182 let oauth2 = OAuth2 {
183 enabled: true,
184 issuer_url: Some("https://example.com".to_string()),
185 client_id: Some("client-id".to_string()),
186 client_secret: Some("client-secret".to_string()),
187 ..Default::default()
188 };
189 assert!(oauth2.validate().is_ok());
190 }
191
192 #[test]
193 fn test_validate_empty_scopes() {
194 let oauth2 = OAuth2 {
195 enabled: true,
196 issuer_url: Some("https://example.com".to_string()),
197 client_id: Some("client-id".to_string()),
198 client_secret: Some("client-secret".to_string()),
199 scopes: vec![],
200 ..Default::default()
201 };
202 let result = oauth2.validate();
203 assert!(result.is_err());
204 assert!(result.unwrap_err().contains("empty"));
205 }
206
207 #[test]
208 fn test_validate_missing_openid_scope() {
209 let oauth2 = OAuth2 {
210 enabled: true,
211 issuer_url: Some("https://example.com".to_string()),
212 client_id: Some("client-id".to_string()),
213 client_secret: Some("client-secret".to_string()),
214 scopes: vec!["profile".to_string(), "email".to_string()],
215 ..Default::default()
216 };
217 let result = oauth2.validate();
218 assert!(result.is_err());
219 assert!(result.unwrap_err().contains("openid"));
220 }
221}