1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use crate::capabilities::CapabilityConfig;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SecurityConfig {
10 pub jwt_config: JwtConfig,
11 pub oauth2_config: Option<OAuth2Config>,
12 pub mfa_config: MfaConfig,
13 pub password_config: PasswordConfig,
14 pub session_config: SessionConfig,
15 pub capability_config: CapabilityConfig,
16 pub rate_limit_config: RateLimitConfig,
17 pub audit_config: AuditConfig,
18}
19
20impl Default for SecurityConfig {
21 fn default() -> Self {
22 Self {
23 jwt_config: JwtConfig::default(),
24 oauth2_config: None,
25 mfa_config: MfaConfig::default(),
26 password_config: PasswordConfig::default(),
27 session_config: SessionConfig::default(),
28 capability_config: CapabilityConfig::default(),
29 rate_limit_config: RateLimitConfig::default(),
30 audit_config: AuditConfig::default(),
31 }
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct JwtConfig {
38 pub algorithm: JwtAlgorithm,
39 pub secret: String,
40 pub issuer: String,
41 pub audience: Vec<String>,
42 pub access_token_expiration: u64, pub refresh_token_expiration: u64, pub leeway_seconds: u64,
45 pub validate_exp: bool,
46 pub validate_nbf: bool,
47 pub validate_aud: bool,
48 pub validate_iss: bool,
49}
50
51impl Default for JwtConfig {
52 fn default() -> Self {
53 Self {
54 algorithm: JwtAlgorithm::HS256,
55 secret: "your-secret-key-change-in-production".to_string(),
56 issuer: "kotoba".to_string(),
57 audience: vec!["kotoba-users".to_string()],
58 access_token_expiration: 900, refresh_token_expiration: 86400, leeway_seconds: 60,
61 validate_exp: true,
62 validate_nbf: false,
63 validate_aud: true,
64 validate_iss: true,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub enum JwtAlgorithm {
72 HS256,
73 HS384,
74 HS512,
75 RS256,
76 RS384,
77 RS512,
78 ES256,
79 ES384,
80 ES512,
81}
82
83impl JwtAlgorithm {
84 pub fn as_str(&self) -> &'static str {
85 match self {
86 JwtAlgorithm::HS256 => "HS256",
87 JwtAlgorithm::HS384 => "HS384",
88 JwtAlgorithm::HS512 => "HS512",
89 JwtAlgorithm::RS256 => "RS256",
90 JwtAlgorithm::RS384 => "RS384",
91 JwtAlgorithm::RS512 => "RS512",
92 JwtAlgorithm::ES256 => "ES256",
93 JwtAlgorithm::ES384 => "ES384",
94 JwtAlgorithm::ES512 => "ES512",
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, Default)]
101pub struct OAuth2Config {
102 #[serde(default)]
103 pub providers: HashMap<String, OAuth2ProviderConfig>,
104 #[serde(default)]
105 pub redirect_uri: String,
106 #[serde(default)]
107 pub scopes: Vec<String>,
108 #[serde(default = "default_state_timeout")]
109 pub state_timeout_seconds: u64,
110}
111
112fn default_state_timeout() -> u64 {
113 600 }
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct OAuth2ProviderConfig {
118 pub client_id: String,
119 pub client_secret: String,
120 pub authorization_url: String,
121 pub token_url: String,
122 pub userinfo_url: Option<String>,
123 pub scope_separator: String,
124 pub additional_params: HashMap<String, String>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct MfaConfig {
130 pub issuer: String,
131 pub digits: u8,
132 pub skew: u8,
133 pub step: u64,
134 pub backup_codes_count: usize,
135 pub qr_code_size: u32,
136}
137
138impl Default for MfaConfig {
139 fn default() -> Self {
140 Self {
141 issuer: "Kotoba".to_string(),
142 digits: 6,
143 skew: 1,
144 step: 30,
145 backup_codes_count: 10,
146 qr_code_size: 200,
147 }
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct PasswordConfig {
154 pub algorithm: PasswordAlgorithm,
155 pub min_length: usize,
156 pub require_uppercase: bool,
157 pub require_lowercase: bool,
158 pub require_digits: bool,
159 pub require_special_chars: bool,
160 pub argon2_config: Option<Argon2Config>,
161 pub pbkdf2_config: Option<Pbkdf2Config>,
162}
163
164impl Default for PasswordConfig {
165 fn default() -> Self {
166 Self {
167 algorithm: PasswordAlgorithm::Argon2,
168 min_length: 8,
169 require_uppercase: true,
170 require_lowercase: true,
171 require_digits: true,
172 require_special_chars: false,
173 argon2_config: Some(Argon2Config::default()),
174 pbkdf2_config: None,
175 }
176 }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[derive(PartialEq)]
181pub enum PasswordAlgorithm {
182 Argon2,
183 Pbkdf2,
184 Bcrypt,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct Argon2Config {
189 pub variant: Argon2Variant,
190 pub version: u32,
191 pub m_cost: u32,
192 pub t_cost: u32,
193 pub p_cost: u32,
194 pub output_len: usize,
195}
196
197impl Default for Argon2Config {
198 fn default() -> Self {
199 Self {
200 variant: Argon2Variant::Argon2id,
201 version: argon2::Version::V0x13 as u32,
202 m_cost: 65536, t_cost: 3,
204 p_cost: 4,
205 output_len: 32,
206 }
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub enum Argon2Variant {
212 Argon2d,
213 Argon2i,
214 Argon2id,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct Pbkdf2Config {
219 pub iterations: u32,
220 pub output_len: usize,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct SessionConfig {
226 pub store_type: SessionStoreType,
227 pub cookie_name: String,
228 pub cookie_secure: bool,
229 pub cookie_http_only: bool,
230 pub cookie_same_site: SameSitePolicy,
231 pub max_age_seconds: Option<u64>,
232 pub idle_timeout_seconds: Option<u64>,
233}
234
235impl Default for SessionConfig {
236 fn default() -> Self {
237 Self {
238 store_type: SessionStoreType::Memory,
239 cookie_name: "kotoba_session".to_string(),
240 cookie_secure: true,
241 cookie_http_only: true,
242 cookie_same_site: SameSitePolicy::Lax,
243 max_age_seconds: Some(86400), idle_timeout_seconds: Some(3600), }
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub enum SessionStoreType {
251 Memory,
252 Redis,
253 Database,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub enum SameSitePolicy {
258 Strict,
259 Lax,
260 None,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct RateLimitConfig {
266 pub enabled: bool,
267 pub max_requests: u32,
268 pub window_seconds: u64,
269 pub burst_size: u32,
270 pub exempt_ips: Vec<String>,
271}
272
273impl Default for RateLimitConfig {
274 fn default() -> Self {
275 Self {
276 enabled: true,
277 max_requests: 100,
278 window_seconds: 60,
279 burst_size: 10,
280 exempt_ips: Vec::new(),
281 }
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct AuditConfig {
288 pub enabled: bool,
289 pub log_level: AuditLogLevel,
290 pub log_sensitive_data: bool,
291 pub retention_days: u64,
292 pub max_entries_per_day: usize,
293}
294
295impl Default for AuditConfig {
296 fn default() -> Self {
297 Self {
298 enabled: true,
299 log_level: AuditLogLevel::Info,
300 log_sensitive_data: false,
301 retention_days: 90,
302 max_entries_per_day: 10000,
303 }
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub enum AuditLogLevel {
309 Debug,
310 Info,
311 Warn,
312 Error,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub enum AuthMethod {
318 Local,
319 OAuth2(String), Ldap,
321 Saml,
322 Custom(String),
323}
324
325impl SecurityConfig {
327 pub fn validate(&self) -> Result<(), String> {
328 if self.jwt_config.secret.is_empty() {
330 return Err("JWT secret cannot be empty".to_string());
331 }
332
333 if self.jwt_config.secret.len() < 32 {
334 return Err("JWT secret should be at least 32 characters long".to_string());
335 }
336
337 if let Some(oauth2) = &self.oauth2_config {
339 if oauth2.providers.is_empty() {
340 return Err("OAuth2 providers cannot be empty".to_string());
341 }
342
343 for (name, provider) in &oauth2.providers {
344 if provider.client_id.is_empty() {
345 return Err(format!("OAuth2 provider '{}' client_id cannot be empty", name));
346 }
347 if provider.client_secret.is_empty() {
348 return Err(format!("OAuth2 provider '{}' client_secret cannot be empty", name));
349 }
350 }
351 }
352
353 if self.password_config.min_length < 8 {
355 return Err("Minimum password length should be at least 8".to_string());
356 }
357
358 Ok(())
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_default_config_validation() {
368 let config = SecurityConfig::default();
369 assert!(config.validate().is_err()); }
371
372 #[test]
373 fn test_config_validation_with_strong_secret() {
374 let mut config = SecurityConfig::default();
375 config.jwt_config.secret = "a".repeat(32);
376 assert!(config.validate().is_ok());
377 }
378
379 #[test]
380 fn test_jwt_algorithm_conversion() {
381 assert_eq!(JwtAlgorithm::HS256.as_str(), "HS256");
382 assert_eq!(JwtAlgorithm::RS256.as_str(), "RS256");
383 assert_eq!(JwtAlgorithm::ES256.as_str(), "ES256");
384 }
385}