1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
|
// Configuration module for SeckelAPI
use anyhow::{Context, Result};
use ipnet::IpNet;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::str::FromStr;
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub database: DatabaseConfig,
pub server: ServerConfig,
pub security: SecurityConfig,
pub permissions: PermissionsConfig,
pub logging: LoggingConfig,
pub auto_generation: Option<HashMap<String, AutoGenerationConfig>>,
pub scheduled_queries: Option<ScheduledQueriesConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PermissionsConfig {
#[serde(flatten)]
pub power_levels: HashMap<String, PowerLevelPermissions>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PowerLevelPermissions {
pub basic_rules: Vec<String>,
pub advanced_rules: Option<Vec<String>>,
pub max_limit: Option<u32>,
pub max_where_conditions: Option<u32>,
pub session_timeout_minutes: Option<u64>,
pub max_concurrent_sessions: Option<u32>,
#[serde(default = "default_true")]
pub rollback_on_error: bool,
#[serde(default = "default_false")]
pub allow_batch_operations: bool,
#[serde(default = "default_user_settings_access")]
pub user_settings_access: UserSettingsAccess,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum UserSettingsAccess {
ReadOwnOnly,
ReadWriteOwn,
ReadWriteAll,
}
fn default_user_settings_access() -> UserSettingsAccess {
UserSettingsAccess::ReadWriteOwn
}
#[derive(Debug, Clone, Deserialize)]
pub struct DatabaseConfig {
pub host: String,
pub port: u16,
pub database: String,
pub username: String,
pub password: String,
#[serde(default = "default_min_connections")]
pub min_connections: u32,
#[serde(default = "default_max_connections")]
pub max_connections: u32,
#[serde(default = "default_connection_timeout_seconds")]
pub connection_timeout_seconds: u64,
#[serde(default = "default_connection_timeout_wait")]
pub connection_timeout_wait: u64,
#[serde(default = "default_connection_check_interval")]
pub connection_check: u64,
}
fn default_min_connections() -> u32 {
1
}
fn default_max_connections() -> u32 {
10
}
fn default_connection_timeout_seconds() -> u64 {
30
}
fn default_connection_timeout_wait() -> u64 {
5
}
fn default_connection_check_interval() -> u64 {
30
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
#[serde(default = "default_request_body_limit_mb")]
pub request_body_limit_mb: usize,
}
fn default_request_body_limit_mb() -> usize {
10 // 10 MB default
}
#[derive(Debug, Clone, Deserialize)]
pub struct SecurityConfig {
pub whitelisted_pin_ips: Vec<String>,
pub whitelisted_string_ips: Vec<String>,
pub session_timeout_minutes: u64,
pub refresh_session_on_activity: bool,
pub max_concurrent_sessions: u32,
pub session_cleanup_interval_minutes: u64,
pub default_max_limit: u32,
pub default_max_where_conditions: u32,
#[serde(default = "default_hash_pins")]
pub hash_pins: bool, // Whether to use bcrypt for PINs (false = plaintext)
#[serde(default = "default_hash_tokens")]
pub hash_tokens: bool, // Whether to use bcrypt for login_strings (false = plaintext)
#[serde(default = "default_pin_column")]
pub pin_column: String, // Database column name for PINs
#[serde(default = "default_token_column")]
pub token_column: String, // Database column name for login strings
// Rate limiting
#[serde(default = "default_enable_rate_limiting")]
pub enable_rate_limiting: bool, // Master switch for rate limiting (disable for debugging)
// Auth rate limiting
#[serde(default = "default_auth_rate_limit_per_minute")]
pub auth_rate_limit_per_minute: u32, // Max auth requests per IP per minute
#[serde(default = "default_auth_rate_limit_per_second")]
pub auth_rate_limit_per_second: u32, // Max auth requests per IP per second (burst protection)
// API rate limiting
#[serde(default = "default_api_rate_limit_per_minute")]
pub api_rate_limit_per_minute: u32, // Max API calls per user per minute
#[serde(default = "default_api_rate_limit_per_second")]
pub api_rate_limit_per_second: u32, // Max API calls per user per second (burst protection)
// Table configuration (moved from basics.toml)
#[serde(default = "default_known_tables")]
pub known_tables: Vec<String>,
#[serde(default = "default_read_only_tables")]
pub read_only_tables: Vec<String>,
#[serde(default = "default_global_write_protected_columns")]
pub global_write_protected_columns: Vec<String>,
// User preferences access control
#[serde(default = "default_user_settings_access")]
pub default_user_settings_access: UserSettingsAccess,
}
fn default_known_tables() -> Vec<String> {
vec![] // Empty by default, must be configured
}
fn default_read_only_tables() -> Vec<String> {
vec![] // Empty by default
}
fn default_global_write_protected_columns() -> Vec<String> {
vec!["id".to_string()] // Protect 'id' by default at minimum
}
fn default_hash_pins() -> bool {
false // Default to plaintext (must be explicitly enabled for bcrypt hashing)
}
fn default_hash_tokens() -> bool {
false // Default to plaintext (must be explicitly enabled for bcrypt hashing)
}
fn default_pin_column() -> String {
"pin_code".to_string()
}
fn default_token_column() -> String {
"login_string".to_string()
}
fn default_enable_rate_limiting() -> bool {
true // Enable by default for security
}
fn default_auth_rate_limit_per_minute() -> u32 {
10 // 10 login attempts per IP per minute (prevents brute force)
}
fn default_auth_rate_limit_per_second() -> u32 {
5 // Max 5 login attempts per second (burst protection)
}
fn default_api_rate_limit_per_minute() -> u32 {
60 // 60 API calls per user per minute
}
fn default_api_rate_limit_per_second() -> u32 {
10 // Max 10 API calls per second (burst protection)
}
#[derive(Debug, Clone, Deserialize)]
pub struct LoggingConfig {
pub request_log: Option<String>,
pub query_log: Option<String>,
pub error_log: Option<String>,
pub warning_log: Option<String>, // Warning messages
pub info_log: Option<String>, // Info messages
pub combined_log: Option<String>, // Unified log with request IDs
pub level: String,
pub mask_passwords: bool,
#[serde(default)]
pub sensitive_fields: Vec<String>, // Fields to mask beyond password/pin
#[serde(default)]
pub custom_filters: Vec<CustomLogFilter>, // Regex-based log routing
}
#[derive(Debug, Clone, Deserialize)]
pub struct CustomLogFilter {
pub name: String,
pub output_file: String,
pub pattern: String, // Regex pattern to match
#[serde(default = "default_filter_enabled")]
pub enabled: bool,
}
fn default_filter_enabled() -> bool {
true
}
fn default_true() -> bool {
true
}
fn default_false() -> bool {
false
}
#[derive(Debug, Clone, Deserialize)]
pub struct AutoGenerationConfig {
pub field: String,
#[serde(rename = "type")]
pub gen_type: String,
pub length: Option<u32>,
pub range_min: Option<u64>,
pub range_max: Option<u64>,
pub max_attempts: Option<u32>,
#[serde(default = "default_on_action")]
pub on_action: String, // "insert", "update", or "both"
}
fn default_on_action() -> String {
"insert".to_string()
}
#[derive(Debug, Clone, Deserialize)]
pub struct ScheduledQueriesConfig {
#[serde(default)]
pub tasks: Vec<ScheduledQueryTask>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ScheduledQueryTask {
pub name: String,
pub description: String,
pub query: String,
pub interval_minutes: u64,
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default = "default_run_on_startup")]
pub run_on_startup: bool,
}
fn default_enabled() -> bool {
true
}
fn default_run_on_startup() -> bool {
true // Run immediately on startup by default
}
impl Config {
pub fn load() -> Result<Self> {
Self::load_from_folder()
}
fn load_from_folder() -> Result<Self> {
// Load consolidated config files
let basics_content =
fs::read_to_string("config/basics.toml").context("Can't read config/basics.toml")?;
let security_content = fs::read_to_string("config/security.toml")
.context("Can't read config/security.toml")?;
let logging_content =
fs::read_to_string("config/logging.toml").context("Can't read config/logging.toml")?;
// Parse individual sections
#[derive(Deserialize)]
struct BasicsWrapper {
server: ServerConfig,
database: DatabaseConfig,
#[serde(default)]
auto_generation: Option<HashMap<String, AutoGenerationConfig>>,
}
#[derive(Deserialize)]
struct SecurityWrapper {
security: SecurityConfig,
permissions: PermissionsConfig,
}
#[derive(Deserialize)]
struct LoggingWrapper {
logging: LoggingConfig,
}
#[derive(Deserialize)]
struct FunctionsWrapper {
#[serde(default)]
auto_generation: Option<HashMap<String, AutoGenerationConfig>>,
#[serde(default)]
scheduled_queries: Option<ScheduledQueriesConfig>,
}
let basics: BasicsWrapper =
toml::from_str(&basics_content).context("Failed to parse basics.toml")?;
let security: SecurityWrapper =
toml::from_str(&security_content).context("Failed to parse security.toml")?;
let logging: LoggingWrapper =
toml::from_str(&logging_content).context("Failed to parse logging.toml")?;
// Load functions.toml if it exists, otherwise use basics fallback
let functions: FunctionsWrapper = if fs::metadata("config/functions.toml").is_ok() {
let functions_content = fs::read_to_string("config/functions.toml")
.context("Can't read config/functions.toml")?;
toml::from_str(&functions_content).context("Failed to parse functions.toml")?
} else {
// Fallback to basics.toml for auto_generation
FunctionsWrapper {
auto_generation: basics.auto_generation.clone(),
scheduled_queries: None,
}
};
let config = Config {
database: basics.database,
server: basics.server,
security: security.security,
permissions: security.permissions,
logging: logging.logging,
auto_generation: functions.auto_generation,
scheduled_queries: functions.scheduled_queries,
};
// Validate configuration
config.validate()?;
Ok(config)
}
/// Validate configuration values
fn validate(&self) -> Result<()> {
// Validate server port (u16 is already limited to 0-65535, just check for 0)
if self.server.port == 0 {
anyhow::bail!(
"Invalid server.port: {} (must be 1-65535)",
self.server.port
);
}
// Validate database port (u16 is already limited to 0-65535, just check for 0)
if self.database.port == 0 {
anyhow::bail!(
"Invalid database.port: {} (must be 1-65535)",
self.database.port
);
}
// Validate database connection details
if self.database.host.trim().is_empty() {
anyhow::bail!("database.host cannot be empty");
}
if self.database.database.trim().is_empty() {
anyhow::bail!("database.database cannot be empty");
}
if self.database.username.trim().is_empty() {
anyhow::bail!("database.username cannot be empty");
}
// Validate PIN whitelist IPs (can be CIDR or single IPs)
for ip in &self.security.whitelisted_pin_ips {
if IpNet::from_str(ip).is_err() && ip.parse::<std::net::IpAddr>().is_err() {
anyhow::bail!("Invalid IP/CIDR in whitelisted_pin_ips: {}", ip);
}
}
// Validate string auth whitelist IPs (can be CIDR or single IPs)
for ip in &self.security.whitelisted_string_ips {
if IpNet::from_str(ip).is_err() && ip.parse::<std::net::IpAddr>().is_err() {
anyhow::bail!("Invalid IP/CIDR in whitelisted_string_ips: {}", ip);
}
}
// Validate session timeout
if self.security.session_timeout_minutes == 0 {
anyhow::bail!("security.session_timeout_minutes must be greater than 0");
}
// Validate permission syntax
for (power_level, perms) in &self.permissions.power_levels {
// Validate power level is numeric
if power_level.parse::<i32>().is_err() {
anyhow::bail!("Invalid power level '{}': must be numeric", power_level);
}
// Validate basic rules format
for rule in &perms.basic_rules {
Self::validate_permission_rule(rule).with_context(|| {
format!("Invalid basic rule for power level {}", power_level)
})?;
}
// Validate advanced rules format
if let Some(advanced_rules) = &perms.advanced_rules {
for rule in advanced_rules {
Self::validate_advanced_rule(rule).with_context(|| {
format!("Invalid advanced rule for power level {}", power_level)
})?;
}
}
}
// Validate logging configuration (paths are now optional)
// No validation needed for optional log paths
// Validate log level
let valid_levels = ["trace", "debug", "info", "warn", "error"];
if !valid_levels.contains(&self.logging.level.to_lowercase().as_str()) {
anyhow::bail!(
"Invalid logging.level '{}': must be one of: {}",
self.logging.level,
valid_levels.join(", ")
);
}
Ok(())
}
/// Validate basic permission rule format (table:permission)
fn validate_permission_rule(rule: &str) -> Result<()> {
let parts: Vec<&str> = rule.split(':').collect();
if parts.len() != 2 {
anyhow::bail!(
"Permission rule '{}' must be in format 'table:permission'",
rule
);
}
let table = parts[0];
let permission = parts[1];
if table.trim().is_empty() {
anyhow::bail!("Table name cannot be empty in rule '{}'", rule);
}
// Validate permission type
let valid_permissions = ["r", "rw", "rwd"];
if !valid_permissions.contains(&permission) {
anyhow::bail!(
"Invalid permission '{}' in rule '{}': must be one of: {}",
permission,
rule,
valid_permissions.join(", ")
);
}
Ok(())
}
/// Validate advanced permission rule format (table.column:permission)
fn validate_advanced_rule(rule: &str) -> Result<()> {
let parts: Vec<&str> = rule.split(':').collect();
if parts.len() != 2 {
anyhow::bail!(
"Advanced rule '{}' must be in format 'table.column:permission'",
rule
);
}
let table_col = parts[0];
let permission = parts[1];
// Validate table.column format
let col_parts: Vec<&str> = table_col.split('.').collect();
if col_parts.len() != 2 {
anyhow::bail!("Advanced rule '{}' must have 'table.column' format", rule);
}
let table = col_parts[0];
let column = col_parts[1];
if table.trim().is_empty() {
anyhow::bail!("Table name cannot be empty in rule '{}'", rule);
}
if column.trim().is_empty() {
anyhow::bail!("Column name cannot be empty in rule '{}'", rule);
}
// Validate permission type
let valid_permissions = ["r", "w", "rw", "block"];
if !valid_permissions.contains(&permission) {
anyhow::bail!(
"Invalid permission '{}' in rule '{}': must be one of: {}",
permission,
rule,
valid_permissions.join(", ")
);
}
Ok(())
}
pub fn get_database_url(&self) -> String {
format!(
"mysql://{}:{}@{}:{}/{}",
self.database.username,
self.database.password,
self.database.host,
self.database.port,
self.database.database
)
}
pub fn is_pin_ip_whitelisted(&self, ip: &str) -> bool {
self.is_ip_in_whitelist(ip, &self.security.whitelisted_pin_ips)
}
pub fn is_string_ip_whitelisted(&self, ip: &str) -> bool {
self.is_ip_in_whitelist(ip, &self.security.whitelisted_string_ips)
}
fn is_ip_in_whitelist(&self, ip: &str, whitelist: &[String]) -> bool {
let client_ip = match ip.parse::<std::net::IpAddr>() {
Ok(addr) => addr,
Err(_) => return false,
};
for allowed in whitelist {
// Try to parse as a network (CIDR notation)
if let Ok(network) = IpNet::from_str(allowed) {
if network.contains(&client_ip) {
return true;
}
}
// Try to parse as a single IP address
else if let Ok(allowed_ip) = allowed.parse::<std::net::IpAddr>() {
if client_ip == allowed_ip {
return true;
}
}
}
false
}
pub fn get_role_permissions(&self, power: i32) -> Vec<(String, String)> {
let power_str = power.to_string();
if let Some(power_perms) = self.permissions.power_levels.get(&power_str) {
power_perms
.basic_rules
.iter()
.filter_map(|perm| {
let parts: Vec<&str> = perm.split(':').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect()
} else {
Vec::new()
}
}
// Helper methods for new configuration sections
pub fn get_known_tables(&self) -> Vec<String> {
if self.security.known_tables.is_empty() {
tracing::warn!("No known_tables configured in security.toml - returning empty list. Wildcard permissions (*) will not work.");
}
self.security.known_tables.clone()
}
pub fn is_read_only_table(&self, table: &str) -> bool {
self.security.read_only_tables.contains(&table.to_string())
}
pub fn get_auto_generation_config(&self, table: &str) -> Option<&AutoGenerationConfig> {
self.auto_generation
.as_ref()
.and_then(|configs| configs.get(table))
}
pub fn get_basic_permissions(&self, power: i32) -> Option<&Vec<String>> {
self.permissions
.power_levels
.get(&power.to_string())
.map(|p| &p.basic_rules)
}
pub fn get_advanced_permissions(&self, power: i32) -> Option<&Vec<String>> {
self.permissions
.power_levels
.get(&power.to_string())
.and_then(|p| p.advanced_rules.as_ref())
}
pub fn filter_readable_columns(
&self,
power: i32,
table: &str,
requested_columns: &[String],
) -> Vec<String> {
if let Some(advanced_rules) = self.get_advanced_permissions(power) {
let mut allowed_columns = Vec::new();
let mut blocked_columns = Vec::new();
let mut has_wildcard_block = false;
let mut has_wildcard_allow = false;
// Parse advanced rules for this table
for rule in advanced_rules {
if let Some((table_col, permission)) = rule.split_once(':') {
if let Some((rule_table, column)) = table_col.split_once('.') {
if rule_table == table {
match permission {
"block" => {
if column == "*" {
has_wildcard_block = true;
} else {
blocked_columns.push(column.to_string());
}
}
"r" | "rw" => {
if column == "*" {
has_wildcard_allow = true;
} else {
allowed_columns.push(column.to_string());
}
}
_ => {}
}
}
}
}
}
// Filter requested columns based on rules
let mut result = Vec::new();
for column in requested_columns {
let allow = if has_wildcard_block {
// If wildcard block, only allow specifically allowed columns
allowed_columns.contains(column)
} else if has_wildcard_allow {
// If wildcard allow, block only specifically blocked columns
!blocked_columns.contains(column)
} else {
// No wildcard rules, block specifically blocked columns
!blocked_columns.contains(column)
};
if allow {
result.push(column.clone());
}
}
result
} else {
// No advanced rules, return all requested columns
requested_columns.to_vec()
}
}
pub fn filter_writable_columns(
&self,
power: i32,
table: &str,
requested_columns: &[String],
) -> Vec<String> {
// First, apply global write-protected columns (these override everything)
let mut globally_blocked: Vec<String> =
self.security.global_write_protected_columns.clone();
if let Some(advanced_rules) = self.get_advanced_permissions(power) {
let mut allowed_columns = Vec::new();
let mut blocked_columns = Vec::new();
let mut has_wildcard_block = false;
let mut has_wildcard_allow = false;
// Parse advanced rules for this table
for rule in advanced_rules {
if let Some((table_col, permission)) = rule.split_once(':') {
if let Some((rule_table, column)) = table_col.split_once('.') {
if rule_table == table {
match permission {
"block" => {
if column == "*" {
has_wildcard_block = true;
} else {
blocked_columns.push(column.to_string());
}
}
"w" | "rw" => {
if column == "*" {
has_wildcard_allow = true;
} else {
allowed_columns.push(column.to_string());
}
}
"r" => {
// Read-only: block from writing but not from reading
blocked_columns.push(column.to_string());
}
_ => {}
}
}
}
}
}
// Merge advanced_rules blocked columns with globally protected
blocked_columns.append(&mut globally_blocked);
// Filter requested columns based on rules
let mut result = Vec::new();
for column in requested_columns {
let allow = if has_wildcard_block {
// If wildcard block, only allow specifically allowed columns
allowed_columns.contains(column)
} else if has_wildcard_allow {
// If wildcard allow, block only specifically blocked columns
!blocked_columns.contains(column)
} else {
// No wildcard rules, block specifically blocked columns
!blocked_columns.contains(column)
};
if allow {
result.push(column.clone());
}
}
result
} else {
// No advanced rules, just filter out globally protected columns
requested_columns
.iter()
.filter(|col| !globally_blocked.contains(col))
.cloned()
.collect()
}
}
/// Get the max_limit for a specific power level (with fallback to next lower power level)
pub fn get_max_limit(&self, power: i32) -> u32 {
// Try exact match first
if let Some(perms) = self.permissions.power_levels.get(&power.to_string()) {
if let Some(limit) = perms.max_limit {
return limit;
}
}
// Find next lower power level
let fallback_power = self.find_fallback_power_level(power);
if let Some(fb_power) = fallback_power {
tracing::warn!(
"Power level {} not found in config, falling back to power level {}",
power,
fb_power
);
if let Some(perms) = self.permissions.power_levels.get(&fb_power.to_string()) {
if let Some(limit) = perms.max_limit {
return limit;
}
}
}
// Ultimate fallback to default
self.security.default_max_limit
}
/// Get the max_where_conditions for a specific power level (with fallback to next lower power level)
pub fn get_max_where_conditions(&self, power: i32) -> u32 {
// Try exact match first
if let Some(perms) = self.permissions.power_levels.get(&power.to_string()) {
if let Some(max_where) = perms.max_where_conditions {
return max_where;
}
}
// Find next lower power level
let fallback_power = self.find_fallback_power_level(power);
if let Some(fb_power) = fallback_power {
tracing::warn!(
"Power level {} not found in config, falling back to power level {}",
power,
fb_power
);
if let Some(perms) = self.permissions.power_levels.get(&fb_power.to_string()) {
if let Some(max_where) = perms.max_where_conditions {
return max_where;
}
}
}
// Ultimate fallback to default
self.security.default_max_where_conditions
}
/// Find the next lower configured power level (e.g., power=60 → fallback to 50)
fn find_fallback_power_level(&self, power: i32) -> Option<i32> {
let mut available_powers: Vec<i32> = self
.permissions
.power_levels
.keys()
.filter_map(|k| k.parse::<i32>().ok())
.filter(|&p| p < power) // Only consider lower power levels
.collect();
available_powers.sort_by(|a, b| b.cmp(a)); // Sort descending
available_powers.first().copied()
}
/// Get the session_timeout_minutes for a specific power level (with fallback to default)
pub fn get_session_timeout(&self, power: i32) -> u64 {
self.permissions
.power_levels
.get(&power.to_string())
.and_then(|p| p.session_timeout_minutes)
.unwrap_or(self.security.session_timeout_minutes)
}
/// Get the max_concurrent_sessions for a specific power level (with fallback to default)
pub fn get_max_concurrent_sessions(&self, power: i32) -> u32 {
self.permissions
.power_levels
.get(&power.to_string())
.and_then(|p| p.max_concurrent_sessions)
.unwrap_or(self.security.max_concurrent_sessions)
}
}
|