Skip to main content

ash_core/config/
scope_policies.rs

1//! Server-side scope policy registry for ASH.
2//!
3//! This module allows servers to define which fields must be protected for each route,
4//! without requiring client-side scope management. The server can enforce that
5//! specific sensitive fields (like `amount`, `recipient`) are always included in
6//! the ASH proof for particular endpoints.
7//!
8//! ## Overview
9//!
10//! Scope policies answer the question: **"For this endpoint, which fields MUST be protected?"**
11//!
12//! ```text
13//! POST /api/transfer  →  ["amount", "recipient"]  // Must protect these fields
14//! POST /api/payment   →  ["amount", "card_last4"]
15//! PUT  /api/users/*   →  ["role", "permissions"]  // Wildcard pattern
16//! ```
17//!
18//! ## Pattern Syntax
19//!
20//! | Pattern | Matches |
21//! |---------|---------|
22//! | `POST\|/api/users\|` | Exact match |
23//! | `GET\|/api/users/*\|` | Single path segment wildcard |
24//! | `GET\|/api/**\|` | Multiple path segments |
25//! | `*\|/api/users\|` | Any HTTP method |
26//! | `PUT\|/api/users/<id>\|` | Named parameter (Express/Flask style) |
27//! | `PUT\|/api/users/{id}\|` | Named parameter (Laravel style) |
28//!
29//! ## Escape Sequences (BUG-020)
30//!
31//! | Escape | Meaning |
32//! |--------|---------|
33//! | `\\*` | Literal asterisk |
34//! | `\\<` | Literal `<` |
35//! | `\\{` | Literal `{` |
36//! | `\\\\` | Literal backslash |
37//!
38//! ## Example
39//!
40//! ```rust
41//! use ash_core::config::{ash_register_scope_policy, ash_get_scope_policy, ash_clear_scope_policies};
42//!
43//! // Clear any existing policies (useful in tests)
44//! ash_clear_scope_policies();
45//!
46//! // Register policies at application startup
47//! ash_register_scope_policy("POST|/api/transfer|", &["amount", "recipient"]);
48//! ash_register_scope_policy("POST|/api/payment|", &["amount", "card_last4"]);
49//! ash_register_scope_policy("PUT|/api/users/*|", &["role", "permissions"]);
50//!
51//! // Later, get policy for a binding
52//! let scope = ash_get_scope_policy("POST|/api/transfer|");
53//! assert_eq!(scope, vec!["amount", "recipient"]);
54//!
55//! // Wildcard matches
56//! let scope = ash_get_scope_policy("PUT|/api/users/123|");
57//! assert_eq!(scope, vec!["role", "permissions"]);
58//! ```
59//!
60//! ## Pattern Priority (BUG-006)
61//!
62//! When multiple patterns could match a binding, the **first registered pattern wins**.
63//! Register more specific patterns before general ones:
64//!
65//! ```rust,ignore
66//! // CORRECT: Specific first
67//! ash_register_scope_policy("POST|/api/admin/users|", &["all_fields"]);
68//! ash_register_scope_policy("POST|/api/*/users|", &["some_fields"]);
69//!
70//! // WRONG: General pattern would match first
71//! ash_register_scope_policy("POST|/api/*/users|", &["some_fields"]);
72//! ash_register_scope_policy("POST|/api/admin/users|", &["all_fields"]); // Never matches!
73//! ```
74//!
75//! ## Security Properties
76//!
77//! | Property | Description |
78//! |----------|-------------|
79//! | **SEC-001** | Regex complexity limits prevent ReDoS |
80//! | **SEC-003** | RwLock poisoning handled gracefully |
81//! | **SEC-007** | Registration order determinism |
82//! | **SEC-009** | Compiled patterns are cached |
83//! | **BUG-006** | First-registered pattern wins |
84//! | **BUG-020** | Proper escape sequence handling |
85
86use regex::Regex;
87use std::collections::{BTreeMap, HashMap};
88use std::sync::RwLock;
89
90/// Maximum allowed pattern length to prevent ReDoS attacks.
91const MAX_PATTERN_LENGTH: usize = 512;
92
93/// Maximum allowed wildcards in a pattern to prevent exponential backtracking.
94const MAX_WILDCARDS: usize = 8;
95
96/// Wildcard characters that have special meaning in patterns.
97const WILDCARD_CHARS: &[char] = &['*', '<', ':', '{'];
98
99/// Check if a wildcard character at position `i` is escaped.
100/// BUG-020: Properly counts consecutive backslashes to handle `\\*` (escaped backslash + wildcard).
101/// An odd number of preceding backslashes means the wildcard is escaped.
102/// An even number means the backslashes are paired (escaped) and the wildcard is unescaped.
103fn ash_is_wildcard_escaped(chars: &[char], i: usize) -> bool {
104    if i == 0 {
105        return false;
106    }
107    // Count consecutive backslashes before this position
108    let mut backslash_count = 0;
109    let mut pos = i - 1;
110    loop {
111        if chars[pos] == '\\' {
112            backslash_count += 1;
113            if pos == 0 {
114                break;
115            }
116            pos -= 1;
117        } else {
118            break;
119        }
120    }
121    // Odd number of backslashes = wildcard is escaped
122    // Even number = backslashes are paired, wildcard is NOT escaped
123    backslash_count % 2 == 1
124}
125
126/// Check if a pattern has any unescaped wildcards.
127fn ash_has_unescaped_wildcard(pattern: &str) -> bool {
128    let chars: Vec<char> = pattern.chars().collect();
129    for (i, &ch) in chars.iter().enumerate() {
130        if WILDCARD_CHARS.contains(&ch) {
131            // BUG-020: Use proper escape detection
132            if !ash_is_wildcard_escaped(&chars, i) {
133                return true;
134            }
135        }
136    }
137    false
138}
139
140/// Count unescaped wildcards in a pattern.
141fn ash_count_unescaped_wildcards(pattern: &str) -> usize {
142    let chars: Vec<char> = pattern.chars().collect();
143    let mut count = 0;
144    for (i, &ch) in chars.iter().enumerate() {
145        if WILDCARD_CHARS.contains(&ch) {
146            // BUG-020: Use proper escape detection
147            if !ash_is_wildcard_escaped(&chars, i) {
148                count += 1;
149            }
150        }
151    }
152    count
153}
154
155/// Unescape a pattern by removing escape backslashes.
156/// BUG-020: Properly handles `\\\\*` (two backslashes + asterisk) as `\*` (literal backslash + literal asterisk).
157/// - `\\` (escaped backslash) becomes `\`
158/// - `\*` (escaped wildcard) becomes `*`
159/// - `\\\\*` becomes `\*` (one literal backslash, one literal asterisk)
160fn ash_unescape_pattern(pattern: &str) -> String {
161    let mut result = String::with_capacity(pattern.len());
162    let chars: Vec<char> = pattern.chars().collect();
163    let mut i = 0;
164    while i < chars.len() {
165        if chars[i] == '\\' && i + 1 < chars.len() {
166            let next = chars[i + 1];
167            if next == '\\' {
168                // Escaped backslash: \\ -> \
169                result.push('\\');
170                i += 2;
171            } else if WILDCARD_CHARS.contains(&next) {
172                // Escaped wildcard: \* -> *
173                result.push(next);
174                i += 2;
175            } else {
176                // Backslash not followed by escapable char: keep as-is
177                result.push(chars[i]);
178                i += 1;
179            }
180        } else {
181            result.push(chars[i]);
182            i += 1;
183        }
184    }
185    result
186}
187
188/// Compiled pattern for efficient matching.
189#[derive(Debug, Clone)]
190struct CompiledPattern {
191    /// Original pattern string
192    pattern: String,
193    /// Compiled regex (None if pattern has no wildcards)
194    regex: Option<Regex>,
195    /// Whether this is an exact match pattern
196    is_exact: bool,
197}
198
199impl CompiledPattern {
200    /// Compile a pattern into a matcher.
201    /// Returns None if the pattern is invalid or too complex.
202    ///
203    /// # Escape Sequences (BUG-010)
204    ///
205    /// Use `\\*` to match a literal asterisk, `\\<` for literal `<`, etc.
206    /// Escaped wildcards don't count toward the wildcard limit.
207    fn compile(pattern: &str) -> Option<Self> {
208        // SEC-001: Limit pattern length to prevent ReDoS
209        if pattern.len() > MAX_PATTERN_LENGTH {
210            return None;
211        }
212
213        // BUG-010: Check for unescaped wildcards only
214        // Count wildcards that are NOT escaped (not preceded by \)
215        let has_unescaped_wildcards = ash_has_unescaped_wildcard(pattern);
216
217        if !has_unescaped_wildcards {
218            // Pattern may have escaped wildcards - exact matching will unescape
219            return Some(CompiledPattern {
220                pattern: pattern.to_string(),
221                regex: None,
222                is_exact: true,
223            });
224        }
225
226        // SEC-001: Count unescaped wildcards to prevent exponential backtracking
227        let wildcard_count = ash_count_unescaped_wildcards(pattern);
228
229        if wildcard_count > MAX_WILDCARDS {
230            return None;
231        }
232
233        // Build regex pattern safely
234        let regex = ash_build_safe_regex(pattern)?;
235
236        Some(CompiledPattern {
237            pattern: pattern.to_string(),
238            regex: Some(regex),
239            is_exact: false,
240        })
241    }
242
243    /// Check if a binding matches this pattern.
244    fn matches(&self, binding: &str) -> bool {
245        if self.is_exact {
246            // BUG-010: For exact match, compare against unescaped pattern
247            return binding == ash_unescape_pattern(&self.pattern);
248        }
249
250        if let Some(ref regex) = self.regex {
251            regex.is_match(binding)
252        } else {
253            false
254        }
255    }
256}
257
258/// Build a safe regex from a pattern, preventing ReDoS.
259///
260/// # BUG-010 & BUG-020: Escape Sequences
261///
262/// Handles escape sequences properly:
263/// - `\\` (escaped backslash) becomes literal `\`
264/// - `\*` (escaped asterisk) becomes literal `*`
265/// - `\\*` (escaped backslash + asterisk) becomes literal `\` + wildcard match
266fn ash_build_safe_regex(pattern: &str) -> Option<Regex> {
267    // Use pre-compiled static patterns for replacements
268    // These patterns match on the ESCAPED regex string
269    // After regex::escape: < stays <, : stays :, { becomes \{, } becomes \}
270    lazy_static::lazy_static! {
271        static ref FLASK_RE: Regex = Regex::new(r"<[a-zA-Z_][a-zA-Z0-9_]*>").unwrap();
272        static ref EXPRESS_RE: Regex = Regex::new(r":[a-zA-Z_][a-zA-Z0-9_]*").unwrap();
273        // In escaped regex, { becomes \{ and } becomes \}
274        // So we need to match the escaped version: \\\{id\\\}
275        static ref LARAVEL_RE: Regex = Regex::new(r"\\\{[a-zA-Z_][a-zA-Z0-9_]*\\\}").unwrap();
276    }
277
278    // BUG-020: Process escape sequences properly by iterating through chars
279    // We need to handle: \\ -> literal backslash, \* -> literal *, etc.
280    let placeholder_backslash = "\x00ESCAPED_BACKSLASH\x00";
281    let placeholder_star = "\x00ESCAPED_STAR\x00";
282    let placeholder_lt = "\x00ESCAPED_LT\x00";
283    let placeholder_colon = "\x00ESCAPED_COLON\x00";
284    let placeholder_lbrace = "\x00ESCAPED_LBRACE\x00";
285
286    let mut temp = String::with_capacity(pattern.len() * 2);
287    let chars: Vec<char> = pattern.chars().collect();
288    let mut i = 0;
289    while i < chars.len() {
290        if chars[i] == '\\' && i + 1 < chars.len() {
291            let next = chars[i + 1];
292            if next == '\\' {
293                // Escaped backslash: \\ -> placeholder for literal backslash
294                temp.push_str(placeholder_backslash);
295                i += 2;
296            } else if next == '*' {
297                temp.push_str(placeholder_star);
298                i += 2;
299            } else if next == '<' {
300                temp.push_str(placeholder_lt);
301                i += 2;
302            } else if next == ':' {
303                temp.push_str(placeholder_colon);
304                i += 2;
305            } else if next == '{' {
306                temp.push_str(placeholder_lbrace);
307                i += 2;
308            } else {
309                temp.push(chars[i]);
310                i += 1;
311            }
312        } else {
313            temp.push(chars[i]);
314            i += 1;
315        }
316    }
317
318    // Now escape for regex
319    let mut regex_str = regex::escape(&temp);
320
321    // Replace ** first (multi-segment) - use non-greedy and limit depth
322    // SEC-001: Use [^|]* instead of .* to prevent catastrophic backtracking
323    regex_str = regex_str.replace(r"\*\*", "[^|]*");
324
325    // Replace * (single segment - not containing | or /)
326    regex_str = regex_str.replace(r"\*", "[^|/]*");
327
328    // Replace route params with bounded character class
329    regex_str = FLASK_RE.replace_all(&regex_str, "[^|/]+").to_string();
330    regex_str = EXPRESS_RE.replace_all(&regex_str, "[^|/]+").to_string();
331    regex_str = LARAVEL_RE.replace_all(&regex_str, "[^|/]+").to_string();
332
333    // BUG-020: Restore escaped characters as literals
334    regex_str = regex_str.replace(&regex::escape(placeholder_backslash), r"\\");
335    regex_str = regex_str.replace(&regex::escape(placeholder_star), r"\*");
336    regex_str = regex_str.replace(&regex::escape(placeholder_lt), "<");
337    regex_str = regex_str.replace(&regex::escape(placeholder_colon), ":");
338    regex_str = regex_str.replace(&regex::escape(placeholder_lbrace), r"\{");
339
340    // SEC-001: Use RegexBuilder with size limit to prevent complex patterns
341    regex::RegexBuilder::new(&format!("^{}$", regex_str))
342        .size_limit(10 * 1024) // 10KB limit on compiled regex size
343        .build()
344        .ok()
345}
346
347/// Scope policy registry that can be instantiated for isolated testing.
348///
349/// For most use cases, use the global functions (`ash_register_scope_policy`, etc.)
350/// which operate on a shared global registry.
351///
352/// # Pattern Priority (BUG-006 fix)
353///
354/// When multiple patterns could match a binding, the **first registered pattern wins**.
355/// This uses registration order, not alphabetical order, for deterministic matching.
356#[derive(Debug, Default)]
357pub struct ScopePolicyRegistry {
358    /// Policies stored in registration order for pattern matching priority.
359    /// BUG-006: Uses Vec to preserve registration order.
360    policies_ordered: Vec<(String, CompiledPattern, Vec<String>)>,
361    /// Fast lookup for exact match bindings.
362    exact_matches: HashMap<String, usize>, // Maps unescaped pattern -> index in policies_ordered
363}
364
365impl ScopePolicyRegistry {
366    /// Create a new empty registry.
367    pub fn new() -> Self {
368        Self {
369            policies_ordered: Vec::new(),
370            exact_matches: HashMap::new(),
371        }
372    }
373
374    /// Register a scope policy for a binding pattern.
375    ///
376    /// Returns false if the pattern is invalid or too complex.
377    ///
378    /// # Pattern Priority (BUG-006)
379    ///
380    /// Later registrations for the same pattern will replace earlier ones.
381    /// When matching, the first registered pattern that matches wins.
382    pub fn register(&mut self, binding: &str, fields: &[&str]) -> bool {
383        if let Some(compiled) = CompiledPattern::compile(binding) {
384            let fields_vec: Vec<String> = fields.iter().map(|s| s.to_string()).collect();
385
386            // Check if this exact pattern was already registered
387            let existing_idx = self.policies_ordered.iter().position(|(p, _, _)| p == binding);
388
389            if let Some(idx) = existing_idx {
390                // MED-001 FIX: Remove old entry from exact_matches if it existed
391                // The old pattern may have been exact while new one is wildcard (or vice versa)
392                let old_unescaped = ash_unescape_pattern(binding);
393                self.exact_matches.remove(&old_unescaped);
394
395                // Update existing entry
396                self.policies_ordered[idx] = (binding.to_string(), compiled.clone(), fields_vec);
397
398                // MED-001 FIX: Re-add to exact_matches if new pattern is exact
399                if compiled.is_exact {
400                    let unescaped = ash_unescape_pattern(binding);
401                    self.exact_matches.insert(unescaped, idx);
402                }
403            } else {
404                // Add new entry
405                let idx = self.policies_ordered.len();
406                self.policies_ordered.push((binding.to_string(), compiled.clone(), fields_vec));
407
408                // BUG-010: For exact matches, store the unescaped pattern in lookup
409                if compiled.is_exact {
410                    let unescaped = ash_unescape_pattern(binding);
411                    self.exact_matches.insert(unescaped, idx);
412                }
413            }
414            true
415        } else {
416            false
417        }
418    }
419
420    /// Register multiple scope policies at once.
421    ///
422    /// Returns the number of successfully registered policies.
423    ///
424    /// # Pattern Priority (BUG-034 Warning)
425    ///
426    /// **Important**: BTreeMap iterates in **alphabetical order** by key, not insertion order.
427    /// When you call this method, patterns are registered in alphabetical order of their
428    /// binding strings.
429    ///
430    /// If you need specific registration order for pattern priority, use multiple calls
431    /// to `register()` instead, or use a `Vec` of tuples and iterate manually.
432    ///
433    /// # Example
434    ///
435    /// ```rust,ignore
436    /// // These will be registered in alphabetical order:
437    /// // 1. "GET|/api/*|"
438    /// // 2. "GET|/api/users|"
439    /// // 3. "POST|/api/*|"
440    /// // NOT the order you insert them into the BTreeMap!
441    /// ```
442    pub fn register_many(&mut self, policies_map: &BTreeMap<&str, Vec<&str>>) -> usize {
443        let mut count = 0;
444        for (binding, fields) in policies_map {
445            if self.register(binding, fields) {
446                count += 1;
447            }
448        }
449        count
450    }
451
452    /// Get the scope policy for a binding.
453    ///
454    /// # Pattern Priority (BUG-006 fix)
455    ///
456    /// Uses registration order - earlier registered patterns take precedence.
457    pub fn get(&self, binding: &str) -> Vec<String> {
458        // Fast path: exact match lookup
459        if let Some(&idx) = self.exact_matches.get(binding) {
460            if idx < self.policies_ordered.len() {
461                return self.policies_ordered[idx].2.clone();
462            }
463        }
464
465        // BUG-006: Iterate in registration order, first match wins
466        for (_, compiled, fields) in &self.policies_ordered {
467            if compiled.matches(binding) {
468                return fields.clone();
469            }
470        }
471
472        // Default: no scoping (full payload protection)
473        Vec::new()
474    }
475
476    /// Check if a binding has a scope policy defined.
477    pub fn has(&self, binding: &str) -> bool {
478        // Fast path: exact match lookup
479        if self.exact_matches.contains_key(binding) {
480            return true;
481        }
482
483        // Check pattern matches in registration order
484        for (_, compiled, _) in &self.policies_ordered {
485            if compiled.matches(binding) {
486                return true;
487            }
488        }
489
490        false
491    }
492
493    /// Get all registered policies.
494    ///
495    /// Returns policies in registration order.
496    pub fn get_all(&self) -> BTreeMap<String, Vec<String>> {
497        self.policies_ordered
498            .iter()
499            .map(|(pattern, _, fields)| (pattern.clone(), fields.clone()))
500            .collect()
501    }
502
503    /// Clear all registered policies.
504    pub fn clear(&mut self) {
505        self.policies_ordered.clear();
506        self.exact_matches.clear();
507    }
508}
509
510lazy_static::lazy_static! {
511    /// Global scope policy registry
512    static ref GLOBAL_REGISTRY: RwLock<ScopePolicyRegistry> = RwLock::new(ScopePolicyRegistry::new());
513}
514
515/// Safely acquire write lock, recovering from poison if needed.
516/// SEC-003: Handle RwLock poisoning gracefully.
517fn ash_get_write_lock() -> std::sync::RwLockWriteGuard<'static, ScopePolicyRegistry> {
518    GLOBAL_REGISTRY
519        .write()
520        .unwrap_or_else(|poisoned| {
521            // Recover from poison by taking the inner value
522            // This allows the system to continue after a panic
523            poisoned.into_inner()
524        })
525}
526
527/// Safely acquire read lock, recovering from poison if needed.
528/// SEC-003: Handle RwLock poisoning gracefully.
529fn ash_get_read_lock() -> std::sync::RwLockReadGuard<'static, ScopePolicyRegistry> {
530    GLOBAL_REGISTRY
531        .read()
532        .unwrap_or_else(|poisoned| {
533            // Recover from poison by taking the inner value
534            poisoned.into_inner()
535        })
536}
537
538/// Register a scope policy for a binding pattern.
539///
540/// # Arguments
541///
542/// * `binding` - The binding pattern (supports `<param>`, `:param`, `{param}`, `*`, `**` wildcards)
543/// * `fields` - The fields that must be protected
544///
545/// # Returns
546///
547/// `true` if the policy was registered, `false` if the pattern is invalid or too complex.
548///
549/// # Security Limits
550///
551/// - Maximum pattern length: 512 characters
552/// - Maximum wildcards: 8 per pattern
553///
554/// # Example
555///
556/// ```rust
557/// use ash_core::config::{ash_register_scope_policy, ash_clear_scope_policies};
558///
559/// ash_clear_scope_policies();
560/// assert!(ash_register_scope_policy("POST|/api/transfer|", &["amount", "recipient"]));
561/// assert!(ash_register_scope_policy("PUT|/api/users/<id>|", &["role", "permissions"]));
562/// ```
563pub fn ash_register_scope_policy(binding: &str, fields: &[&str]) -> bool {
564    let mut registry = ash_get_write_lock();
565    registry.register(binding, fields)
566}
567
568/// Register multiple scope policies at once.
569///
570/// # Arguments
571///
572/// * `policies` - Map of binding => fields
573///
574/// # Returns
575///
576/// Number of successfully registered policies.
577///
578/// # Pattern Priority (BUG-034 Warning)
579///
580/// **Important**: BTreeMap iterates in **alphabetical order** by key, not insertion order.
581/// Patterns are registered in alphabetical order of their binding strings.
582///
583/// If you need specific registration order for pattern priority (since "first registered
584/// pattern wins"), use multiple calls to `ash_register_scope_policy()` instead.
585///
586/// # Example
587///
588/// ```rust
589/// use std::collections::BTreeMap;
590/// use ash_core::config::{ash_register_scope_policies, ash_clear_scope_policies};
591///
592/// ash_clear_scope_policies();
593/// let mut policies = BTreeMap::new();
594/// policies.insert("POST|/api/transfer|", vec!["amount", "recipient"]);
595/// policies.insert("POST|/api/payment|", vec!["amount", "card_last4"]);
596/// assert_eq!(ash_register_scope_policies(&policies), 2);
597/// // Note: These are registered in alphabetical order!
598/// ```
599pub fn ash_register_scope_policies(policies_map: &BTreeMap<&str, Vec<&str>>) -> usize {
600    let mut registry = ash_get_write_lock();
601    registry.register_many(policies_map)
602}
603
604/// Get the scope policy for a binding.
605///
606/// Returns empty vector if no policy is defined (full payload protection).
607///
608/// # Arguments
609///
610/// * `binding` - The normalized binding string
611///
612/// # Returns
613///
614/// The fields that must be protected
615///
616/// # Example
617///
618/// ```rust
619/// use ash_core::config::{ash_register_scope_policy, ash_get_scope_policy, ash_clear_scope_policies};
620///
621/// ash_clear_scope_policies();
622/// ash_register_scope_policy("POST|/api/transfer|", &["amount", "recipient"]);
623///
624/// let scope = ash_get_scope_policy("POST|/api/transfer|");
625/// assert_eq!(scope, vec!["amount", "recipient"]);
626///
627/// let no_scope = ash_get_scope_policy("GET|/api/users|");
628/// assert!(no_scope.is_empty());
629/// ```
630pub fn ash_get_scope_policy(binding: &str) -> Vec<String> {
631    let registry = ash_get_read_lock();
632    registry.get(binding)
633}
634
635/// Check if a binding has a scope policy defined.
636///
637/// # Arguments
638///
639/// * `binding` - The normalized binding string
640///
641/// # Returns
642///
643/// True if a policy exists
644pub fn ash_has_scope_policy(binding: &str) -> bool {
645    let registry = ash_get_read_lock();
646    registry.has(binding)
647}
648
649/// Get all registered policies.
650///
651/// # Returns
652///
653/// All registered scope policies in deterministic order.
654pub fn ash_get_all_scope_policies() -> BTreeMap<String, Vec<String>> {
655    let registry = ash_get_read_lock();
656    registry.get_all()
657}
658
659/// Clear all registered policies.
660///
661/// Useful for testing.
662pub fn ash_clear_scope_policies() {
663    let mut registry = ash_get_write_lock();
664    registry.clear();
665}
666
667// =========================================================================
668// Deprecated Aliases for Backward Compatibility
669// =========================================================================
670
671// =========================================================================
672// Deprecated Aliases for Backward Compatibility (Non-conforming names)
673// =========================================================================
674
675/// Register a scope policy for a binding pattern.
676///
677/// # Deprecated
678///
679/// Use [`ash_register_scope_policy`] instead. This alias is provided for backward compatibility.
680/// Note: This function name does not follow the `ash_` prefix convention and will be removed in v3.0.
681#[deprecated(since = "2.4.0", note = "Use ash_register_scope_policy instead")]
682pub fn register_scope_policy(binding: &str, fields: &[&str]) -> bool {
683    ash_register_scope_policy(binding, fields)
684}
685
686/// Register multiple scope policies at once.
687///
688/// # Deprecated
689///
690/// Use [`ash_register_scope_policies`] instead. This alias is provided for backward compatibility.
691/// Note: This function name does not follow the `ash_` prefix convention and will be removed in v3.0.
692#[deprecated(since = "2.4.0", note = "Use ash_register_scope_policies instead")]
693pub fn register_scope_policies(policies_map: &BTreeMap<&str, Vec<&str>>) -> usize {
694    ash_register_scope_policies(policies_map)
695}
696
697/// Get the scope policy for a binding.
698///
699/// # Deprecated
700///
701/// Use [`ash_get_scope_policy`] instead. This alias is provided for backward compatibility.
702/// Note: This function name does not follow the `ash_` prefix convention and will be removed in v3.0.
703#[deprecated(since = "2.4.0", note = "Use ash_get_scope_policy instead")]
704pub fn get_scope_policy(binding: &str) -> Vec<String> {
705    ash_get_scope_policy(binding)
706}
707
708/// Check if a binding has a scope policy defined.
709///
710/// # Deprecated
711///
712/// Use [`ash_has_scope_policy`] instead. This alias is provided for backward compatibility.
713/// Note: This function name does not follow the `ash_` prefix convention and will be removed in v3.0.
714#[deprecated(since = "2.4.0", note = "Use ash_has_scope_policy instead")]
715pub fn has_scope_policy(binding: &str) -> bool {
716    ash_has_scope_policy(binding)
717}
718
719/// Get all registered policies.
720///
721/// # Deprecated
722///
723/// Use [`ash_get_all_scope_policies`] instead. This alias is provided for backward compatibility.
724/// Note: This function name does not follow the `ash_` prefix convention and will be removed in v3.0.
725#[deprecated(since = "2.4.0", note = "Use ash_get_all_scope_policies instead")]
726pub fn get_all_scope_policies() -> BTreeMap<String, Vec<String>> {
727    ash_get_all_scope_policies()
728}
729
730/// Clear all registered policies.
731///
732/// # Deprecated
733///
734/// Use [`ash_clear_scope_policies`] instead. This alias is provided for backward compatibility.
735/// Note: This function name does not follow the `ash_` prefix convention and will be removed in v3.0.
736#[deprecated(since = "2.4.0", note = "Use ash_clear_scope_policies instead")]
737pub fn clear_scope_policies() {
738    ash_clear_scope_policies()
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744
745    // Tests using isolated ScopePolicyRegistry instances - can run in parallel
746
747    #[test]
748    fn test_registry_register_and_get() {
749        let mut registry = ScopePolicyRegistry::new();
750        assert!(registry.register("POST|/api/transfer|", &["amount", "recipient"]));
751
752        let scope = registry.get("POST|/api/transfer|");
753        assert_eq!(scope, vec!["amount", "recipient"]);
754    }
755
756    #[test]
757    fn test_registry_get_no_match() {
758        let registry = ScopePolicyRegistry::new();
759
760        let scope = registry.get("GET|/api/users|");
761        assert!(scope.is_empty());
762    }
763
764    #[test]
765    fn test_registry_has() {
766        let mut registry = ScopePolicyRegistry::new();
767        registry.register("POST|/api/transfer|", &["amount"]);
768
769        assert!(registry.has("POST|/api/transfer|"));
770        assert!(!registry.has("GET|/api/users|"));
771    }
772
773    #[test]
774    fn test_registry_pattern_matching_flask_style() {
775        let mut registry = ScopePolicyRegistry::new();
776        registry.register("PUT|/api/users/<id>|", &["role", "permissions"]);
777
778        let scope = registry.get("PUT|/api/users/123|");
779        assert_eq!(scope, vec!["role", "permissions"]);
780    }
781
782    #[test]
783    fn test_registry_pattern_matching_express_style() {
784        let mut registry = ScopePolicyRegistry::new();
785        registry.register("PUT|/api/users/:id|", &["role"]);
786
787        let scope = registry.get("PUT|/api/users/456|");
788        assert_eq!(scope, vec!["role"]);
789    }
790
791    #[test]
792    fn test_registry_pattern_matching_laravel_style() {
793        let mut registry = ScopePolicyRegistry::new();
794        registry.register("PUT|/api/users/{id}|", &["email"]);
795
796        let scope = registry.get("PUT|/api/users/789|");
797        assert_eq!(scope, vec!["email"]);
798    }
799
800    #[test]
801    fn test_registry_pattern_matching_wildcard() {
802        let mut registry = ScopePolicyRegistry::new();
803        registry.register("POST|/api/*/transfer|", &["amount"]);
804
805        let scope = registry.get("POST|/api/v1/transfer|");
806        assert_eq!(scope, vec!["amount"]);
807    }
808
809    #[test]
810    fn test_registry_pattern_matching_double_wildcard() {
811        let mut registry = ScopePolicyRegistry::new();
812        registry.register("POST|/api/**/transfer|", &["amount"]);
813
814        let scope = registry.get("POST|/api/v1/users/transfer|");
815        assert_eq!(scope, vec!["amount"]);
816    }
817
818    #[test]
819    fn test_registry_clear() {
820        let mut registry = ScopePolicyRegistry::new();
821        registry.register("POST|/api/transfer|", &["amount"]);
822
823        assert!(registry.has("POST|/api/transfer|"));
824
825        registry.clear();
826
827        assert!(!registry.has("POST|/api/transfer|"));
828    }
829
830    #[test]
831    fn test_registry_register_many() {
832        let mut registry = ScopePolicyRegistry::new();
833        let mut policies = BTreeMap::new();
834        policies.insert("POST|/api/transfer|", vec!["amount"]);
835        policies.insert("POST|/api/payment|", vec!["card"]);
836
837        let count = registry.register_many(&policies);
838        assert_eq!(count, 2);
839
840        assert!(registry.has("POST|/api/transfer|"));
841        assert!(registry.has("POST|/api/payment|"));
842    }
843
844    #[test]
845    fn test_registry_get_all() {
846        let mut registry = ScopePolicyRegistry::new();
847        registry.register("POST|/api/transfer|", &["amount"]);
848        registry.register("POST|/api/payment|", &["card"]);
849
850        let all = registry.get_all();
851        assert_eq!(all.len(), 2);
852    }
853
854    // Security tests
855
856    #[test]
857    fn test_rejects_pattern_too_long() {
858        let mut registry = ScopePolicyRegistry::new();
859        let long_pattern = "POST|/api/".to_string() + &"a".repeat(600) + "|";
860
861        // Should reject pattern that's too long (SEC-001)
862        assert!(!registry.register(&long_pattern, &["field"]));
863    }
864
865    #[test]
866    fn test_rejects_too_many_wildcards() {
867        let mut registry = ScopePolicyRegistry::new();
868        let many_wildcards = "POST|/*/*/*/*/*/*/*/*/*|"; // 9 wildcards
869
870        // Should reject pattern with too many wildcards (SEC-001)
871        assert!(!registry.register(many_wildcards, &["field"]));
872    }
873
874    #[test]
875    fn test_accepts_valid_wildcards() {
876        let mut registry = ScopePolicyRegistry::new();
877        let valid_pattern = "POST|/*/*/*/*/*/*/*|"; // 7 wildcards (within limit)
878
879        // Should accept pattern within limits
880        assert!(registry.register(valid_pattern, &["field"]));
881    }
882
883    // BUG-020: Escape sequence handling tests
884
885    #[test]
886    fn test_escaped_backslash_before_wildcard() {
887        let mut registry = ScopePolicyRegistry::new();
888        // Pattern: \\* means literal backslash + wildcard (match any)
889        // This should match "POST|/api\anything|"
890        assert!(registry.register(r"POST|/api\\*|", &["field"]));
891
892        // Should match path with literal backslash followed by anything
893        let scope = registry.get(r"POST|/api\test|");
894        assert_eq!(scope, vec!["field"]);
895
896        let scope2 = registry.get(r"POST|/api\foo|");
897        assert_eq!(scope2, vec!["field"]);
898    }
899
900    #[test]
901    fn test_escaped_asterisk_exact_match() {
902        let mut registry = ScopePolicyRegistry::new();
903        // Pattern: \* means literal asterisk (exact match)
904        assert!(registry.register(r"POST|/api/\*|", &["field"]));
905
906        // Should match path with literal asterisk
907        let scope = registry.get("POST|/api/*|");
908        assert_eq!(scope, vec!["field"]);
909
910        // Should NOT match other paths
911        let scope2 = registry.get("POST|/api/test|");
912        assert!(scope2.is_empty());
913    }
914
915    #[test]
916    fn test_double_escaped_backslash() {
917        let mut registry = ScopePolicyRegistry::new();
918        // Pattern: \\\\ means two literal backslashes (exact match)
919        assert!(registry.register(r"POST|/api/\\\\test|", &["field"]));
920
921        // Should match path with two literal backslashes
922        let scope = registry.get(r"POST|/api/\\test|");
923        assert_eq!(scope, vec!["field"]);
924    }
925
926    #[test]
927    fn test_is_wildcard_escaped_helper() {
928        // Test the helper function directly
929        let chars1: Vec<char> = r"\*".chars().collect();
930        assert!(ash_is_wildcard_escaped(&chars1, 1)); // * is escaped
931
932        let chars2: Vec<char> = r"\\*".chars().collect();
933        assert!(!ash_is_wildcard_escaped(&chars2, 2)); // * is NOT escaped (backslash is escaped)
934
935        let chars3: Vec<char> = r"\\\*".chars().collect();
936        assert!(ash_is_wildcard_escaped(&chars3, 3)); // * is escaped
937
938        let chars4: Vec<char> = "*".chars().collect();
939        assert!(!ash_is_wildcard_escaped(&chars4, 0)); // * is NOT escaped (no preceding backslash)
940    }
941}