nacm_validator/
lib.rs

1//! # NACM Validator - Network Access Control Model with Tail-f ACM Extensions
2//!
3//! This library implements NACM (RFC 8341) access control validation in Rust with comprehensive
4//! support for Tail-f ACM (Access Control Model) extensions. It provides functionality to:
5//!
6//! ## Core NACM Features (RFC 8341)
7//! - Parse real-world NACM XML configurations
8//! - Validate access requests against defined rules
9//! - Handle user groups and rule precedence
10//! - Support various operations (CRUD + exec) and path matching
11//!
12//! ## Tail-f ACM Extensions
13//! - **Command Rules**: Context-aware command access control (CLI, WebUI, NETCONF)
14//! - **Enhanced Logging**: Granular logging control with `log-if-*` attributes
15//! - **ValidationResult**: Returns both access decision and logging indication
16//! - **Group ID Mapping**: External authentication system integration via GID
17//! - **Context Awareness**: Different access policies for different user interfaces
18//!
19//! ## Quick Start
20//!
21//! ### Standard NACM Data Access Validation
22//! 
23//! ```rust
24//! use nacm_validator::{NacmConfig, AccessRequest, Operation, RequestContext};
25//!
26//! // Load configuration from XML
27//! let xml_content = r#"<?xml version="1.0" encoding="UTF-8"?>
28//! <config xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
29//!   <nacm>
30//!     <enable-nacm>true</enable-nacm>
31//!     <read-default>permit</read-default>
32//!     <write-default>deny</write-default>
33//!     <exec-default>permit</exec-default>
34//!     <groups>
35//!       <group>
36//!         <name>admin</name>
37//!         <user-name>alice</user-name>
38//!       </group>
39//!     </groups>
40//!     <rule-list>
41//!       <name>admin-acl</name>
42//!       <group>admin</group>
43//!       <rule>
44//!         <name>permit-all</name>
45//!         <action>permit</action>
46//!       </rule>
47//!     </rule-list>
48//!   </nacm>
49//! </config>"#;
50//! let config = NacmConfig::from_xml(&xml_content)?;
51//!
52//! // Create a data access request
53//! let context = RequestContext::NETCONF;
54//! let request = AccessRequest {
55//!     user: "alice",
56//!     module_name: Some("ietf-interfaces"),
57//!     rpc_name: None,
58//!     operation: Operation::Read,
59//!     path: Some("/interfaces"),
60//!     context: Some(&context),
61//!     command: None,
62//! };
63//!
64//! // Validate the request - returns ValidationResult with access decision and logging info
65//! let result = config.validate(&request);
66//! println!("Access {}: {}", 
67//!          if result.effect == nacm_validator::RuleEffect::Permit { "PERMIT" } else { "DENY" },
68//!          if result.should_log { "[LOGGED]" } else { "" });
69//! # Ok::<(), Box<dyn std::error::Error>>(())
70//! ```
71//!
72//! ### Tail-f ACM Command Access Validation
73//!
74//! ```rust
75//! use nacm_validator::{NacmConfig, AccessRequest, Operation, RequestContext};
76//!
77//! // Load configuration with Tail-f ACM command rules
78//! let xml_content = r#"<?xml version="1.0" encoding="UTF-8"?>
79//! <config xmlns="http://tail-f.com/ns/config/1.0">
80//!     <nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
81//!         <enable-nacm>true</enable-nacm>
82//!         <read-default>deny</read-default>
83//!         <write-default>deny</write-default>
84//!         <exec-default>deny</exec-default>
85//!         <cmd-read-default xmlns="http://tail-f.com/yang/acm">permit</cmd-read-default>
86//!         <cmd-exec-default xmlns="http://tail-f.com/yang/acm">deny</cmd-exec-default>
87//!         <groups>
88//!             <group>
89//!                 <name>admin</name>
90//!                 <user-name>alice</user-name>
91//!             </group>
92//!         </groups>
93//!         <rule-list>
94//!             <name>admin-rules</name>
95//!             <group>admin</group>
96//!             <cmdrule xmlns="http://tail-f.com/yang/acm">
97//!                 <name>cli-show</name>
98//!                 <context>cli</context>
99//!                 <command>show *</command>
100//!                 <action>permit</action>
101//!             </cmdrule>
102//!         </rule-list>
103//!     </nacm>
104//! </config>"#;
105//! let config = NacmConfig::from_xml(&xml_content)?;
106//!
107//! // Create a command access request
108//! let context = RequestContext::CLI;
109//! let request = AccessRequest {
110//!     user: "alice",
111//!     module_name: None,
112//!     rpc_name: None,
113//!     operation: Operation::Read,
114//!     path: None,
115//!     context: Some(&context),
116//!     command: Some("show status"),
117//! };
118//!
119//! // Validate command access using Tail-f ACM command rules
120//! let result = config.validate(&request);
121//! match result.effect {
122//!     nacm_validator::RuleEffect::Permit => {
123//!         println!("Command access PERMITTED{}", 
124//!                 if result.should_log { " [LOGGED]" } else { "" });
125//!     },
126//!     nacm_validator::RuleEffect::Deny => {
127//!         println!("Command access DENIED{}", 
128//!                 if result.should_log { " [LOGGED]" } else { "" });
129//!     }
130//! }
131//! # Ok::<(), Box<dyn std::error::Error>>(())
132//! ```
133
134use serde::{Deserialize, Serialize};
135use std::collections::{HashMap, HashSet};
136
137/// NACM Rule effect (permit or deny)
138/// 
139/// This enum represents the final decision for an access request.
140/// In NACM, every rule must have an action that either permits or denies access.
141/// 
142/// # Examples
143/// 
144/// ```
145/// use nacm_validator::RuleEffect;
146/// 
147/// let permit = RuleEffect::Permit;
148/// let deny = RuleEffect::Deny;
149/// 
150/// // Rules with permit effects allow access
151/// assert_eq!(permit == RuleEffect::Permit, true);
152/// ```
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "lowercase")] // Serializes as "permit"/"deny" in JSON/XML
155pub enum RuleEffect {
156    /// Allow the requested access
157    Permit,
158    /// Deny the requested access
159    Deny,
160}
161
162/// Access control validation result with logging indication
163/// 
164/// This structure contains both the access control decision and whether
165/// the decision should be logged according to the configured logging rules.
166/// This supports the Tail-f ACM extensions that provide fine-grained control
167/// over access control logging.
168/// 
169/// # Examples
170/// 
171/// ```
172/// use nacm_validator::{ValidationResult, RuleEffect};
173/// 
174/// let result = ValidationResult {
175///     effect: RuleEffect::Permit,
176///     should_log: true,
177/// };
178/// 
179/// if result.should_log {
180///     println!("Access {}: should be logged", 
181///              if result.effect == RuleEffect::Permit { "permitted" } else { "denied" });
182/// }
183/// ```
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub struct ValidationResult {
186    /// The access control decision
187    pub effect: RuleEffect,
188    /// Whether this decision should be logged
189    pub should_log: bool,
190}
191
192/// Implementation of `FromStr` trait for `RuleEffect`
193/// 
194/// This allows parsing rule effects from strings (used when parsing XML).
195/// Case-insensitive parsing: "PERMIT", "permit", "Permit" all work.
196impl std::str::FromStr for RuleEffect {
197    type Err = String;
198    
199    fn from_str(s: &str) -> Result<Self, Self::Err> {
200        match s.trim().to_lowercase().as_str() {
201            "permit" => Ok(RuleEffect::Permit),
202            "deny" => Ok(RuleEffect::Deny),
203            _ => Err(format!("Unknown rule effect: {}", s)),
204        }
205    }
206}
207
208/// NACM operations enumeration
209/// 
210/// Represents the different types of operations that can be performed
211/// in a NETCONF/RESTCONF system. Maps to standard CRUD operations plus exec.
212/// 
213/// # Examples
214/// 
215/// ```
216/// use nacm_validator::Operation;
217/// 
218/// let read_op = Operation::Read;
219/// let write_op = Operation::Update;
220/// 
221/// // Operations can be compared
222/// assert_ne!(read_op, write_op);
223/// ```
224#[derive(Debug, Clone, PartialEq, Eq, Hash)]
225pub enum Operation {
226    /// Reading/retrieving data (GET operations)
227    Read,
228    /// Creating new data (POST operations)
229    Create,
230    /// Modifying existing data (PUT/PATCH operations)
231    Update,
232    /// Removing data (DELETE operations)
233    Delete,
234    /// Executing RPCs or actions (RPC operations)
235    Exec,
236}
237
238/// Request context enumeration
239/// 
240/// Represents the different management interfaces or contexts from which
241/// an access request originates. This is part of the Tail-f ACM extensions
242/// that enable context-specific access control rules.
243/// 
244/// # Examples
245/// 
246/// ```
247/// use nacm_validator::RequestContext;
248/// 
249/// let cli_context = RequestContext::CLI;
250/// let netconf_context = RequestContext::NETCONF;
251/// ```
252#[derive(Debug, Clone, PartialEq, Eq, Hash)]
253pub enum RequestContext {
254    /// NETCONF protocol access
255    NETCONF,
256    /// Command-line interface access
257    CLI,
258    /// Web-based user interface access
259    WebUI,
260    /// Other/custom interface
261    Other(String),
262}
263
264impl RequestContext {
265    /// Check if this context matches a pattern
266    /// 
267    /// Supports wildcard matching where "*" matches any context.
268    /// 
269    /// # Arguments
270    /// 
271    /// * `pattern` - The pattern to match against (e.g., "cli", "*", "webui")
272    /// 
273    /// # Returns
274    /// 
275    /// * `true` if the context matches the pattern
276    /// * `false` otherwise
277    pub fn matches(&self, pattern: &str) -> bool {
278        if pattern == "*" {
279            return true;
280        }
281        
282        match self {
283            RequestContext::NETCONF => pattern.eq_ignore_ascii_case("netconf"),
284            RequestContext::CLI => pattern.eq_ignore_ascii_case("cli"),
285            RequestContext::WebUI => pattern.eq_ignore_ascii_case("webui"),
286            RequestContext::Other(name) => pattern.eq_ignore_ascii_case(name),
287        }
288    }
289}
290
291/// Implementation of `FromStr` trait for `Operation`
292/// 
293/// Enables parsing operations from strings, used in CLI and XML parsing.
294/// Case-insensitive: "READ", "read", "Read" all parse to `Operation::Read`.
295impl std::str::FromStr for Operation {
296    type Err = String;
297    
298    fn from_str(s: &str) -> Result<Self, Self::Err> {
299        match s.trim().to_lowercase().as_str() {
300            "read" => Ok(Operation::Read),
301            "create" => Ok(Operation::Create),
302            "update" => Ok(Operation::Update),
303            "delete" => Ok(Operation::Delete),
304            "exec" => Ok(Operation::Exec),
305            _ => Err(format!("Unknown operation: {}", s)),
306        }
307    }
308}
309
310/// NACM Rule structure (extended to match XML format)
311/// 
312/// Represents a single NACM access control rule. Each rule defines:
313/// - What it applies to (module, RPC, path)
314/// - Which operations it covers
315/// - Whether it permits or denies access
316/// - Its precedence order (lower numbers = higher priority)
317/// 
318/// # Fields
319/// 
320/// * `name` - Human-readable identifier for the rule
321/// * `module_name` - YANG module this rule applies to (None = any module)
322/// * `rpc_name` - Specific RPC name (None = any RPC, "*" = wildcard)
323/// * `path` - XPath or data path (None = any path, "/" = root)
324/// * `access_operations` - Set of operations this rule covers
325/// * `effect` - Whether to permit or deny matching requests
326/// * `order` - Rule precedence (lower = higher priority)
327/// * `context` - Request context this rule applies to (Tail-f extension)
328/// * `log_if_permit` - Log when this rule permits access (Tail-f extension)
329/// * `log_if_deny` - Log when this rule denies access (Tail-f extension)
330/// 
331/// # Examples
332/// 
333/// ```
334/// use nacm_validator::{NacmRule, RuleEffect, Operation};
335/// use std::collections::HashSet;
336/// 
337/// let mut ops = HashSet::new();
338/// ops.insert(Operation::Read);
339/// 
340/// let rule = NacmRule {
341///     name: "allow-read-interfaces".to_string(),
342///     module_name: Some("ietf-interfaces".to_string()),
343///     rpc_name: None,
344///     path: Some("/interfaces".to_string()),
345///     access_operations: ops,
346///     effect: RuleEffect::Permit,
347///     order: 10,
348///     context: None,
349///     log_if_permit: false,
350///     log_if_deny: false,
351/// };
352/// ```
353#[derive(Debug, Clone)]
354pub struct NacmRule {
355    /// Unique name for this rule
356    pub name: String,
357    /// YANG module name this rule applies to (None = any module)
358    pub module_name: Option<String>,
359    /// RPC name this rule applies to (None = any RPC)
360    pub rpc_name: Option<String>,
361    /// XPath or data path (None = any path)
362    pub path: Option<String>,
363    /// Set of operations covered by this rule
364    pub access_operations: HashSet<Operation>,
365    /// Whether this rule permits or denies access
366    pub effect: RuleEffect,
367    /// Rule precedence - lower numbers have higher priority
368    pub order: u32,
369    /// Request context pattern this rule applies to (Tail-f extension)
370    pub context: Option<String>,
371    /// Log when this rule permits access (Tail-f extension)
372    pub log_if_permit: bool,
373    /// Log when this rule denies access (Tail-f extension)
374    pub log_if_deny: bool,
375}
376
377/// NACM Command Rule structure (Tail-f ACM extension)
378/// 
379/// Represents a command-based access control rule for CLI and Web UI operations.
380/// Command rules complement standard NACM data access rules by controlling
381/// access to management commands that don't map to NETCONF operations.
382/// 
383/// # Fields
384/// 
385/// * `name` - Human-readable identifier for the command rule
386/// * `context` - Management interface pattern (e.g., "cli", "webui", "*")
387/// * `command` - Command pattern to match (supports wildcards)
388/// * `access_operations` - Set of command operations (read, exec)
389/// * `effect` - Whether to permit or deny matching command requests
390/// * `order` - Rule precedence within the rule list
391/// * `log_if_permit` - Log when this rule permits access
392/// * `log_if_deny` - Log when this rule denies access
393/// * `comment` - Optional description of the rule
394/// 
395/// # Examples
396/// 
397/// ```
398/// use nacm_validator::{NacmCommandRule, RuleEffect, Operation};
399/// use std::collections::HashSet;
400/// 
401/// let mut ops = HashSet::new();
402/// ops.insert(Operation::Read);
403/// ops.insert(Operation::Exec);
404/// 
405/// let cmd_rule = NacmCommandRule {
406///     name: "cli-show-status".to_string(),
407///     context: Some("cli".to_string()),
408///     command: Some("show status".to_string()),
409///     access_operations: ops,
410///     effect: RuleEffect::Permit,
411///     order: 10,
412///     log_if_permit: true,
413///     log_if_deny: false,
414///     comment: Some("Allow operators to view system status".to_string()),
415/// };
416/// ```
417#[derive(Debug, Clone)]
418pub struct NacmCommandRule {
419    /// Unique name for this command rule
420    pub name: String,
421    /// Management interface pattern (e.g., "cli", "webui", "*")
422    pub context: Option<String>,
423    /// Command pattern to match (supports wildcards)
424    pub command: Option<String>,
425    /// Set of command operations covered by this rule
426    pub access_operations: HashSet<Operation>,
427    /// Whether this rule permits or denies access
428    pub effect: RuleEffect,
429    /// Rule precedence within the rule list
430    pub order: u32,
431    /// Log when this rule permits access
432    pub log_if_permit: bool,
433    /// Log when this rule denies access
434    pub log_if_deny: bool,
435    /// Optional description of the rule
436    pub comment: Option<String>,
437}
438
439/// NACM Rule List with associated groups
440/// 
441/// A rule list is a named collection of rules that applies to specific user groups.
442/// Rule lists are processed in order, and within each list, rules are ordered by priority.
443/// 
444/// # Fields
445/// 
446/// * `name` - Identifier for this rule list
447/// * `groups` - User groups this rule list applies to
448/// * `rules` - Ordered list of access control rules
449/// * `command_rules` - Ordered list of command access control rules (Tail-f extension)
450/// 
451/// # Examples
452/// 
453/// ```
454/// use nacm_validator::{NacmRuleList, NacmRule, NacmCommandRule, RuleEffect, Operation};
455/// use std::collections::HashSet;
456/// 
457/// let rule_list = NacmRuleList {
458///     name: "admin-rules".to_string(),
459///     groups: vec!["admin".to_string()],
460///     rules: vec![], // Would contain actual rules
461///     command_rules: vec![], // Would contain command rules
462/// };
463/// ```
464#[derive(Debug, Clone)]
465pub struct NacmRuleList {
466    /// Name of this rule list
467    pub name: String,
468    /// User groups this rule list applies to
469    pub groups: Vec<String>,
470    /// Ordered list of rules in this list
471    pub rules: Vec<NacmRule>,
472    /// Ordered list of command rules in this list (Tail-f extension)
473    pub command_rules: Vec<NacmCommandRule>,
474}
475
476/// NACM Group definition
477/// 
478/// Represents a named group of users. Groups are used to organize users
479/// and apply rule lists to multiple users at once.
480/// 
481/// # Fields
482/// 
483/// * `name` - Group identifier (e.g., "admin", "operators")
484/// * `users` - List of usernames belonging to this group
485/// * `gid` - Optional numerical group ID for OS integration (Tail-f extension)
486/// 
487/// # Examples
488/// 
489/// ```
490/// use nacm_validator::NacmGroup;
491/// 
492/// let admin_group = NacmGroup {
493///     name: "admin".to_string(),
494///     users: vec!["alice".to_string(), "bob".to_string()],
495///     gid: Some(1000),
496/// };
497/// ```
498#[derive(Debug, Clone)]
499pub struct NacmGroup {
500    /// Name of the group
501    pub name: String,
502    /// List of usernames in this group
503    pub users: Vec<String>,
504    /// Optional numerical group ID for OS integration (Tail-f extension)
505    pub gid: Option<i32>,
506}
507
508/// Full NACM configuration
509/// 
510/// The main configuration object that contains all NACM settings:
511/// - Global enable/disable flag
512/// - Default policies for different operation types
513/// - User groups and their members
514/// - Rule lists with access control rules
515/// 
516/// # Fields
517/// 
518/// * `enable_nacm` - Global NACM enable flag
519/// * `read_default` - Default policy for read operations
520/// * `write_default` - Default policy for write operations (create/update/delete)
521/// * `exec_default` - Default policy for exec operations (RPC calls)
522/// * `cmd_read_default` - Default policy for command read operations (Tail-f extension)
523/// * `cmd_exec_default` - Default policy for command exec operations (Tail-f extension)
524/// * `log_if_default_permit` - Log when default policies permit access (Tail-f extension)
525/// * `log_if_default_deny` - Log when default policies deny access (Tail-f extension)
526/// * `groups` - Map of group names to group definitions
527/// * `rule_lists` - List of rule lists, processed in order
528/// 
529/// # Examples
530/// 
531/// ```
532/// use nacm_validator::{NacmConfig, RuleEffect};
533/// use std::collections::HashMap;
534/// 
535/// let config = NacmConfig {
536///     enable_nacm: true,
537///     read_default: RuleEffect::Deny,
538///     write_default: RuleEffect::Deny,
539///     exec_default: RuleEffect::Deny,
540///     cmd_read_default: RuleEffect::Permit,
541///     cmd_exec_default: RuleEffect::Permit,
542///     log_if_default_permit: false,
543///     log_if_default_deny: false,
544///     groups: HashMap::new(),
545///     rule_lists: vec![],
546/// };
547/// ```
548#[derive(Debug, Clone)]
549pub struct NacmConfig {
550    /// Global NACM enable flag - if false, all access is permitted
551    pub enable_nacm: bool,
552    /// Default policy for read operations when no rules match
553    pub read_default: RuleEffect,
554    /// Default policy for write operations (create/update/delete) when no rules match
555    pub write_default: RuleEffect,
556    /// Default policy for exec operations (RPCs) when no rules match
557    pub exec_default: RuleEffect,
558    /// Default policy for command read operations when no command rules match (Tail-f extension)
559    pub cmd_read_default: RuleEffect,
560    /// Default policy for command exec operations when no command rules match (Tail-f extension)
561    pub cmd_exec_default: RuleEffect,
562    /// Log when default policies permit access (Tail-f extension)
563    pub log_if_default_permit: bool,
564    /// Log when default policies deny access (Tail-f extension)
565    pub log_if_default_deny: bool,
566    /// Map of group name to group definition
567    pub groups: HashMap<String, NacmGroup>,
568    /// Ordered list of rule lists
569    pub rule_lists: Vec<NacmRuleList>,
570}
571
572/// Represents an access request for validation
573/// 
574/// This structure contains all the information needed to validate
575/// an access request against NACM rules. Uses borrowed string slices
576/// for efficiency (avoids copying strings).
577/// 
578/// # Lifetimes
579/// 
580/// The `'a` lifetime parameter ensures that this struct doesn't outlive
581/// the data it references. This is Rust's way of preventing dangling pointers.
582/// 
583/// # Fields
584/// 
585/// * `user` - Username making the request
586/// * `module_name` - YANG module being accessed (if applicable)
587/// * `rpc_name` - RPC being called (if applicable)
588/// * `operation` - Type of operation being performed
589/// * `path` - Data path being accessed (if applicable)
590/// * `context` - Request context (NETCONF, CLI, WebUI, etc.) - Tail-f extension
591/// * `command` - Command being executed (for command rules) - Tail-f extension
592/// 
593/// # Examples
594/// 
595/// ```
596/// use nacm_validator::{AccessRequest, Operation, RequestContext};
597/// 
598/// let request = AccessRequest {
599///     user: "alice",
600///     module_name: Some("ietf-interfaces"),
601///     rpc_name: None,
602///     operation: Operation::Read,
603///     path: Some("/interfaces/interface[name='eth0']"),
604///     context: Some(&RequestContext::NETCONF),
605///     command: None,
606/// };
607/// ```
608pub struct AccessRequest<'a> {
609    /// Username making the access request
610    pub user: &'a str,
611    /// YANG module name being accessed (None if not module-specific)
612    pub module_name: Option<&'a str>,
613    /// RPC name being called (None if not an RPC call)
614    pub rpc_name: Option<&'a str>,
615    /// Type of operation being performed
616    pub operation: Operation,
617    /// XPath or data path being accessed (None if not path-specific)
618    pub path: Option<&'a str>,
619    /// Request context (NETCONF, CLI, WebUI, etc.) - Tail-f extension
620    pub context: Option<&'a RequestContext>,
621    /// Command being executed (for command rules) - Tail-f extension
622    pub command: Option<&'a str>,
623}
624
625// ============================================================================
626// XML Parsing Structures
627// ============================================================================
628//
629// The following structures are used internally for parsing XML configuration
630// files. They mirror the XML schema and use serde attributes to handle
631// the conversion from XML elements to Rust structs.
632//
633// These are separate from the main API structs to:
634// 1. Handle XML-specific naming (kebab-case vs snake_case)
635// 2. Deal with XML structure differences (nested elements, attributes)
636// 3. Keep the public API clean and independent of XML format
637//
638// The #[derive(Deserialize)] enables automatic XML parsing via serde-xml-rs.
639// The #[serde(rename = "...")] attributes map XML element names to Rust fields.
640// ============================================================================
641
642/// Root XML configuration element
643/// 
644/// Maps to the top-level `<config>` element in NACM XML files.
645/// Contains the main `<nacm>` configuration block.
646#[derive(Debug, Deserialize)]
647struct XmlConfig {
648    /// The main NACM configuration block
649    #[serde(rename = "nacm")]
650    pub nacm: XmlNacm,
651}
652
653/// Main NACM configuration element from XML
654/// 
655/// Maps to the `<nacm>` element and contains all NACM settings:
656/// global flags, default policies, groups, and rule lists.
657#[derive(Debug, Deserialize)]
658struct XmlNacm {
659    /// Global NACM enable flag (XML: <enable-nacm>)
660    #[serde(rename = "enable-nacm")]
661    pub enable_nacm: bool,
662    /// Default policy for read operations (XML: <read-default>)
663    #[serde(rename = "read-default")]
664    pub read_default: String,
665    /// Default policy for write operations (XML: <write-default>)
666    #[serde(rename = "write-default")]
667    pub write_default: String,
668    /// Default policy for exec operations (XML: <exec-default>)
669    #[serde(rename = "exec-default")]
670    pub exec_default: String,
671    /// Default policy for command read operations (XML: <cmd-read-default>) - Tail-f extension
672    #[serde(rename = "cmd-read-default", default = "default_permit")]
673    pub cmd_read_default: String,
674    /// Default policy for command exec operations (XML: <cmd-exec-default>) - Tail-f extension
675    #[serde(rename = "cmd-exec-default", default = "default_permit")]
676    pub cmd_exec_default: String,
677    /// Log when default policies permit access (XML: <log-if-default-permit/>) - Tail-f extension
678    #[serde(rename = "log-if-default-permit", default)]
679    pub log_if_default_permit: Option<()>,
680    /// Log when default policies deny access (XML: <log-if-default-deny/>) - Tail-f extension
681    #[serde(rename = "log-if-default-deny", default)]
682    pub log_if_default_deny: Option<()>,
683    /// Container for all groups (XML: <groups>)
684    pub groups: XmlGroups,
685    /// List of rule lists (XML: <rule-list> elements)
686    #[serde(rename = "rule-list")]
687    pub rule_lists: Vec<XmlRuleList>,
688}
689
690/// Default function for cmd-read-default and cmd-exec-default
691fn default_permit() -> String {
692    "permit".to_string()
693}
694
695/// Container for group definitions from XML
696/// 
697/// Maps to the `<groups>` element which contains multiple `<group>` elements.
698#[derive(Debug, Deserialize)]
699struct XmlGroups {
700    /// List of individual group definitions
701    pub group: Vec<XmlGroup>,
702}
703
704/// Individual group definition from XML
705/// 
706/// Maps to a `<group>` element containing group name and user list.
707#[derive(Debug, Deserialize)]
708struct XmlGroup {
709    /// Group name (XML: <name>)
710    pub name: String,
711    /// List of usernames in this group (XML: <user-name> elements)
712    /// The `default` attribute provides an empty vector if no users are specified
713    #[serde(rename = "user-name", default)]
714    pub user_names: Vec<String>,
715    /// Optional numerical group ID (XML: <gid>) - Tail-f extension
716    #[serde(default)]
717    pub gid: Option<i32>,
718}
719
720/// Rule list definition from XML
721/// 
722/// Maps to a `<rule-list>` element containing the rule list metadata
723/// and the actual access control rules.
724#[derive(Debug, Deserialize)]
725struct XmlRuleList {
726    /// Rule list name (XML: <name>)
727    pub name: String,
728    /// Group this rule list applies to (XML: <group>)
729    pub group: String,
730    /// List of rules in this rule list (XML: <rule> elements)
731    /// The `default` attribute provides an empty vector if no rules are specified
732    #[serde(default)]
733    pub rule: Vec<XmlRule>,
734    /// List of command rules in this rule list (XML: <cmdrule> elements) - Tail-f extension
735    #[serde(default)]
736    pub cmdrule: Vec<XmlCommandRule>,
737}
738
739/// Individual access control rule from XML
740/// 
741/// Maps to a `<rule>` element with all its sub-elements.
742/// Optional fields use `Option<T>` to handle missing XML elements.
743#[derive(Debug, Deserialize)]
744struct XmlRule {
745    /// Rule name (XML: <name>)
746    pub name: String,
747    /// YANG module name this rule applies to (XML: <module-name>)
748    #[serde(rename = "module-name")]
749    pub module_name: Option<String>,
750    /// RPC name this rule applies to (XML: <rpc-name>)
751    #[serde(rename = "rpc-name")]
752    pub rpc_name: Option<String>,
753    /// XPath or data path (XML: <path>)
754    pub path: Option<String>,
755    /// Space-separated list of operations (XML: <access-operations>)
756    #[serde(rename = "access-operations")]
757    pub access_operations: Option<String>,
758    /// Rule effect: "permit" or "deny" (XML: <action>)
759    pub action: String,
760    /// Request context pattern (XML: <context>) - Tail-f extension
761    #[serde(default)]
762    pub context: Option<String>,
763    /// Log when this rule permits access (XML: <log-if-permit/>) - Tail-f extension
764    #[serde(rename = "log-if-permit", default)]
765    pub log_if_permit: Option<()>,
766    /// Log when this rule denies access (XML: <log-if-deny/>) - Tail-f extension
767    #[serde(rename = "log-if-deny", default)]
768    pub log_if_deny: Option<()>,
769}
770
771/// Individual command access control rule from XML (Tail-f extension)
772/// 
773/// Maps to a `<cmdrule>` element with all its sub-elements.
774/// Optional fields use `Option<T>` to handle missing XML elements.
775#[derive(Debug, Deserialize)]
776struct XmlCommandRule {
777    /// Command rule name (XML: <name>)
778    pub name: String,
779    /// Management interface pattern (XML: <context>)
780    #[serde(default)]
781    pub context: Option<String>,
782    /// Command pattern to match (XML: <command>)
783    #[serde(default)]
784    pub command: Option<String>,
785    /// Space-separated list of command operations (XML: <access-operations>)
786    #[serde(rename = "access-operations")]
787    pub access_operations: Option<String>,
788    /// Rule effect: "permit" or "deny" (XML: <action>)
789    pub action: String,
790    /// Log when this rule permits access (XML: <log-if-permit/>)
791    #[serde(rename = "log-if-permit", default)]
792    pub log_if_permit: Option<()>,
793    /// Log when this rule denies access (XML: <log-if-deny/>)
794    #[serde(rename = "log-if-deny", default)]
795    pub log_if_deny: Option<()>,
796    /// Optional description (XML: <comment>)
797    #[serde(default)]
798    pub comment: Option<String>,
799}
800
801impl NacmConfig {
802    /// Parse NACM configuration from XML string
803    /// 
804    /// This function takes an XML string containing NACM configuration
805    /// and parses it into a `NacmConfig` struct. It handles the conversion
806    /// from the XML schema to our internal representation.
807    /// 
808    /// # Arguments
809    /// 
810    /// * `xml_content` - String slice containing the XML configuration
811    /// 
812    /// # Returns
813    /// 
814    /// * `Ok(NacmConfig)` - Successfully parsed configuration
815    /// * `Err(Box<dyn Error>)` - Parsing failed (malformed XML, unknown values, etc.)
816    /// 
817    /// # Examples
818    /// 
819    /// ```rust
820    /// use nacm_validator::NacmConfig;
821    /// 
822    /// let xml = r#"
823    /// <config xmlns="http://tail-f.com/ns/config/1.0">
824    ///   <nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
825    ///     <enable-nacm>true</enable-nacm>
826    ///     <read-default>deny</read-default>
827    ///     <write-default>deny</write-default>
828    ///     <exec-default>deny</exec-default>
829    ///     <groups>
830    ///       <group>
831    ///         <name>admin</name>
832    ///         <user-name>alice</user-name>
833    ///       </group>
834    ///     </groups>
835    ///     <rule-list>
836    ///       <name>admin-rules</name>
837    ///       <group>admin</group>
838    ///     </rule-list>
839    ///   </nacm>
840    /// </config>
841    /// "#;
842    /// 
843    /// let config = NacmConfig::from_xml(xml).unwrap();
844    /// assert_eq!(config.enable_nacm, true);
845    /// ```
846    pub fn from_xml(xml_content: &str) -> Result<Self, Box<dyn std::error::Error>> {
847        // Step 1: Parse XML into intermediate structures
848        // serde_xml_rs automatically deserializes the XML based on our struct definitions
849        let xml_config: XmlConfig = serde_xml_rs::from_str(xml_content)?;
850        
851        // Step 2: Convert XML groups to our internal representation
852        // Transform from XML format to HashMap for efficient lookups
853        let mut groups = HashMap::new();
854        for xml_group in xml_config.nacm.groups.group {
855            // Create internal group representation and add to HashMap for O(1) lookup
856            groups.insert(xml_group.name.clone(), NacmGroup {
857                name: xml_group.name,
858                users: xml_group.user_names,
859                gid: xml_group.gid, // Tail-f extension
860            });
861        }
862        
863        // Step 3: Convert XML rule lists to our internal representation
864        // Process each rule list and assign ordering for rule precedence
865        let mut rule_lists = Vec::new();
866        for (order_base, xml_rule_list) in xml_config.nacm.rule_lists.iter().enumerate() {
867            let mut rules = Vec::new();
868            
869            // Process each rule within this rule list
870            for (rule_order, xml_rule) in xml_rule_list.rule.iter().enumerate() {
871                // Step 3a: Parse access operations from string format
872                // Handle both wildcard ("*") and space-separated operation lists
873                let mut access_operations = HashSet::new();
874                if let Some(ops_str) = &xml_rule.access_operations {
875                    if ops_str.trim() == "*" {
876                        // Wildcard means all operations
877                        access_operations.insert(Operation::Read);
878                        access_operations.insert(Operation::Create);
879                        access_operations.insert(Operation::Update);
880                        access_operations.insert(Operation::Delete);
881                        access_operations.insert(Operation::Exec);
882                    } else {
883                        // Parse space-separated operation names like "read write"
884                        for op in ops_str.split_whitespace() {
885                            if let Ok(operation) = op.parse::<Operation>() {
886                                access_operations.insert(operation);
887                            }
888                            // Note: Invalid operations are silently ignored
889                        }
890                    }
891                }
892                
893                // Step 3b: Parse the rule effect (permit/deny)
894                let effect = xml_rule.action.parse::<RuleEffect>()?;
895                
896                // Step 3c: Create internal rule representation
897                // Calculate rule order: rule list order * 1000 + rule position
898                // This ensures rules in earlier rule lists have higher priority
899                rules.push(NacmRule {
900                    name: xml_rule.name.clone(),
901                    module_name: xml_rule.module_name.clone(),
902                    rpc_name: xml_rule.rpc_name.clone(),
903                    path: xml_rule.path.clone(),
904                    access_operations,
905                    effect,
906                    // Calculate rule priority: list_position * 1000 + rule_position
907                    // This ensures proper ordering across multiple rule lists
908                    order: (order_base * 1000 + rule_order) as u32,
909                    context: xml_rule.context.clone(), // Tail-f extension
910                    log_if_permit: xml_rule.log_if_permit.is_some(), // Tail-f extension
911                    log_if_deny: xml_rule.log_if_deny.is_some(), // Tail-f extension
912                });
913            }
914            
915            // Process command rules within this rule list (Tail-f extension)
916            let mut command_rules = Vec::new();
917            for (cmd_rule_order, xml_cmd_rule) in xml_rule_list.cmdrule.iter().enumerate() {
918                // Parse command access operations
919                let mut cmd_access_operations = HashSet::new();
920                if let Some(ops_str) = &xml_cmd_rule.access_operations {
921                    if ops_str.trim() == "*" {
922                        // For command rules, wildcard typically means read and exec
923                        cmd_access_operations.insert(Operation::Read);
924                        cmd_access_operations.insert(Operation::Exec);
925                    } else {
926                        // Parse space-separated operation names like "read exec"
927                        for op in ops_str.split_whitespace() {
928                            if let Ok(operation) = op.parse::<Operation>() {
929                                cmd_access_operations.insert(operation);
930                            }
931                        }
932                    }
933                } else {
934                    // Default to all command operations if not specified
935                    cmd_access_operations.insert(Operation::Read);
936                    cmd_access_operations.insert(Operation::Exec);
937                }
938                
939                // Parse command rule effect
940                let cmd_effect = xml_cmd_rule.action.parse::<RuleEffect>()?;
941                
942                // Create internal command rule representation
943                command_rules.push(NacmCommandRule {
944                    name: xml_cmd_rule.name.clone(),
945                    context: xml_cmd_rule.context.clone(),
946                    command: xml_cmd_rule.command.clone(),
947                    access_operations: cmd_access_operations,
948                    effect: cmd_effect,
949                    order: (order_base * 1000 + cmd_rule_order) as u32,
950                    log_if_permit: xml_cmd_rule.log_if_permit.is_some(),
951                    log_if_deny: xml_cmd_rule.log_if_deny.is_some(),
952                    comment: xml_cmd_rule.comment.clone(),
953                });
954            }
955            
956            // Step 3d: Create the rule list with its associated group
957            // Note: XML format has single group per rule list, our internal format supports multiple
958            rule_lists.push(NacmRuleList {
959                name: xml_rule_list.name.clone(),
960                groups: vec![xml_rule_list.group.clone()],  // Wrap single group in Vec
961                rules,
962                command_rules, // Tail-f extension
963            });
964        }
965        
966        // Step 4: Create the final configuration object
967        // Parse default policies from strings and assemble everything
968        Ok(NacmConfig {
969            enable_nacm: xml_config.nacm.enable_nacm,
970            // Parse default policy strings ("permit"/"deny") to enum values
971            read_default: xml_config.nacm.read_default.parse()?,
972            write_default: xml_config.nacm.write_default.parse()?,
973            exec_default: xml_config.nacm.exec_default.parse()?,
974            // Parse Tail-f command default policies
975            cmd_read_default: xml_config.nacm.cmd_read_default.parse()?,
976            cmd_exec_default: xml_config.nacm.cmd_exec_default.parse()?,
977            // Parse Tail-f logging settings (empty elements become true if present)
978            log_if_default_permit: xml_config.nacm.log_if_default_permit.is_some(),
979            log_if_default_deny: xml_config.nacm.log_if_default_deny.is_some(),
980            groups,
981            rule_lists,
982        })
983    }
984    
985    /// Validate an access request against the NACM configuration
986    /// 
987    /// This is the main validation function that determines whether an access
988    /// request should be permitted or denied based on the NACM rules, including
989    /// command rules from the Tail-f ACM extensions.
990    /// 
991    /// # Algorithm
992    /// 
993    /// 1. If NACM is disabled globally, permit all access
994    /// 2. Find all groups the user belongs to
995    /// 3. If this is a command request, check command rules first
996    /// 4. Otherwise, check standard NACM data access rules
997    /// 5. Sort rules by precedence (order field)
998    /// 6. Return the effect and logging info of the first matching rule
999    /// 7. If no rules match, apply the appropriate default policy
1000    /// 
1001    /// # Arguments
1002    /// 
1003    /// * `req` - The access request to validate
1004    /// 
1005    /// # Returns
1006    /// 
1007    /// * `ValidationResult` - Contains the access decision and logging flag
1008    /// 
1009    /// # Examples
1010    /// 
1011    /// ```rust
1012    /// use nacm_validator::{NacmConfig, AccessRequest, Operation, RequestContext, ValidationResult, RuleEffect};
1013    /// 
1014    /// # let config = NacmConfig {
1015    /// #     enable_nacm: true,
1016    /// #     read_default: RuleEffect::Deny,
1017    /// #     write_default: RuleEffect::Deny,
1018    /// #     exec_default: RuleEffect::Deny,
1019    /// #     cmd_read_default: RuleEffect::Permit,
1020    /// #     cmd_exec_default: RuleEffect::Permit,
1021    /// #     log_if_default_permit: false,
1022    /// #     log_if_default_deny: false,
1023    /// #     groups: std::collections::HashMap::new(),
1024    /// #     rule_lists: vec![],
1025    /// # };
1026    /// let request = AccessRequest {
1027    ///     user: "alice",
1028    ///     module_name: Some("ietf-interfaces"),
1029    ///     rpc_name: None,
1030    ///     operation: Operation::Read,
1031    ///     path: Some("/interfaces"),
1032    ///     context: Some(&RequestContext::NETCONF),
1033    ///     command: None,
1034    /// };
1035    /// 
1036    /// let result = config.validate(&request);
1037    /// // Result contains both the access decision and logging flag
1038    /// ```
1039    pub fn validate(&self, req: &AccessRequest) -> ValidationResult {
1040        // Step 1: If NACM is disabled, permit all access without logging
1041        if !self.enable_nacm {
1042            return ValidationResult {
1043                effect: RuleEffect::Permit,
1044                should_log: false,
1045            };
1046        }
1047        
1048        // Step 2: Find all groups this user belongs to
1049        // Uses functional programming style with iterator chains
1050        let user_groups: Vec<&str> = self.groups
1051            .iter()                    // Iterator over (group_name, group) pairs
1052            .filter_map(|(group_name, group)| {  // Transform and filter in one step
1053                if group.users.contains(&req.user.to_string()) {
1054                    Some(group_name.as_str())  // Include this group name
1055                } else {
1056                    None                       // Skip this group
1057                }
1058            })
1059            .collect();                // Collect into a Vec
1060        
1061        // Step 3: Check if this is a command request
1062        if req.command.is_some() {
1063            return self.validate_command_request(req, &user_groups);
1064        }
1065        
1066        // Step 4: Standard NACM data access validation
1067        self.validate_data_request(req, &user_groups)
1068    }
1069    
1070    /// Validate a command access request (Tail-f ACM extension)
1071    /// 
1072    /// This helper function specifically handles command rule validation
1073    /// for CLI, WebUI, and other command-based access requests.
1074    /// 
1075    /// # Arguments
1076    /// 
1077    /// * `req` - The access request containing command information
1078    /// * `user_groups` - List of groups the user belongs to
1079    /// 
1080    /// # Returns
1081    /// 
1082    /// * `ValidationResult` - Contains the access decision and logging flag
1083    fn validate_command_request(&self, req: &AccessRequest, user_groups: &[&str]) -> ValidationResult {
1084        let mut matching_cmd_rules = Vec::new();
1085        
1086        // Collect all matching command rules from applicable rule lists
1087        for rule_list in &self.rule_lists {
1088            // Check if this rule list applies to any of the user's groups
1089            let applies = rule_list.groups.iter().any(|group| {
1090                group == "*" || user_groups.contains(&group.as_str())
1091            });
1092            
1093            if applies {
1094                // Check each command rule in this rule list
1095                for cmd_rule in &rule_list.command_rules {
1096                    if self.command_rule_matches(cmd_rule, req) {
1097                        matching_cmd_rules.push(cmd_rule);
1098                    }
1099                }
1100            }
1101        }
1102        
1103        // Sort command rules by precedence (lower order = higher priority)
1104        matching_cmd_rules.sort_by_key(|r| r.order);
1105        
1106        // Return the effect of the first matching command rule
1107        if let Some(cmd_rule) = matching_cmd_rules.first() {
1108            let should_log = match cmd_rule.effect {
1109                RuleEffect::Permit => cmd_rule.log_if_permit,
1110                RuleEffect::Deny => cmd_rule.log_if_deny,
1111            };
1112            
1113            ValidationResult {
1114                effect: cmd_rule.effect,
1115                should_log,
1116            }
1117        } else {
1118            // No command rules matched - apply command default policy
1119            let default_effect = match req.operation {
1120                Operation::Read => self.cmd_read_default,
1121                _ => self.cmd_exec_default, // All other operations default to exec policy
1122            };
1123            
1124            let should_log = match default_effect {
1125                RuleEffect::Permit => self.log_if_default_permit,
1126                RuleEffect::Deny => self.log_if_default_deny,
1127            };
1128            
1129            ValidationResult {
1130                effect: default_effect,
1131                should_log,
1132            }
1133        }
1134    }
1135    
1136    /// Validate a data access request (standard NACM)
1137    /// 
1138    /// This helper function handles standard NACM data access rule validation
1139    /// for NETCONF and similar protocol-based requests.
1140    /// 
1141    /// # Arguments
1142    /// 
1143    /// * `req` - The access request containing data access information
1144    /// * `user_groups` - List of groups the user belongs to
1145    /// 
1146    /// # Returns
1147    /// 
1148    /// * `ValidationResult` - Contains the access decision and logging flag
1149    fn validate_data_request(&self, req: &AccessRequest, user_groups: &[&str]) -> ValidationResult {
1150        let mut matching_rules = Vec::new();
1151        
1152        // Collect all matching rules from applicable rule lists
1153        for rule_list in &self.rule_lists {
1154            // Check if this rule list applies to any of the user's groups
1155            let applies = rule_list.groups.iter().any(|group| {
1156                group == "*" || user_groups.contains(&group.as_str())
1157            });
1158            
1159            if applies {
1160                // Check each rule in this rule list
1161                for rule in &rule_list.rules {
1162                    if self.rule_matches(rule, req) {
1163                        matching_rules.push(rule);
1164                    }
1165                }
1166            }
1167        }
1168        
1169        // Sort rules by precedence (lower order = higher priority)
1170        matching_rules.sort_by_key(|r| r.order);
1171        
1172        // Return the effect of the first matching rule
1173        if let Some(rule) = matching_rules.first() {
1174            let should_log = match rule.effect {
1175                RuleEffect::Permit => rule.log_if_permit,
1176                RuleEffect::Deny => rule.log_if_deny,
1177            };
1178            
1179            ValidationResult {
1180                effect: rule.effect,
1181                should_log,
1182            }
1183        } else {
1184            // No rules matched - apply default policy based on operation type
1185            let default_effect = match req.operation {
1186                Operation::Read => self.read_default,
1187                // Group write operations together (create/update/delete)
1188                Operation::Create | Operation::Update | Operation::Delete => self.write_default,
1189                Operation::Exec => self.exec_default,
1190            };
1191            
1192            let should_log = match default_effect {
1193                RuleEffect::Permit => self.log_if_default_permit,
1194                RuleEffect::Deny => self.log_if_default_deny,
1195            };
1196            
1197            ValidationResult {
1198                effect: default_effect,
1199                should_log,
1200            }
1201        }
1202    }
1203    
1204    /// Check if a command rule matches an access request (Tail-f ACM extension)
1205    /// 
1206    /// This private helper function determines whether a specific command rule
1207    /// applies to a given access request. A command rule matches if ALL of its
1208    /// conditions are satisfied (AND logic).
1209    /// 
1210    /// # Matching Logic
1211    /// 
1212    /// * **Operations**: Rule must cover the requested operation
1213    /// * **Context**: Rule's context must match the request context (or be wildcard)
1214    /// * **Command**: Rule's command pattern must match the requested command
1215    /// 
1216    /// # Arguments
1217    /// 
1218    /// * `cmd_rule` - The command rule to check
1219    /// * `req` - The access request to match against
1220    /// 
1221    /// # Returns
1222    /// 
1223    /// * `true` if the command rule matches the request
1224    /// * `false` if any condition fails
1225    fn command_rule_matches(&self, cmd_rule: &NacmCommandRule, req: &AccessRequest) -> bool {
1226        // Check 1: Operations - Rule must cover the requested operation
1227        if !cmd_rule.access_operations.is_empty() && !cmd_rule.access_operations.contains(&req.operation) {
1228            return false;
1229        }
1230        
1231        // Check 2: Context matching
1232        if let Some(rule_context) = &cmd_rule.context {
1233            if let Some(req_context) = req.context {
1234                if !req_context.matches(rule_context) {
1235                    return false;
1236                }
1237            } else if rule_context != "*" {
1238                // Rule specifies context but request has none
1239                return false;
1240            }
1241        }
1242        
1243        // Check 3: Command matching
1244        if let Some(rule_command) = &cmd_rule.command {
1245            if let Some(req_command) = req.command {
1246                if !self.command_matches(rule_command, req_command) {
1247                    return false;
1248                }
1249            } else if rule_command != "*" {
1250                // Rule specifies command but request has none
1251                return false;
1252            }
1253        }
1254        
1255        true
1256    }
1257    
1258    /// Check if a command pattern matches a requested command
1259    /// 
1260    /// Implements command matching logic supporting:
1261    /// - Exact string matching
1262    /// - Wildcard matching with '*'
1263    /// - Prefix matching for command hierarchies
1264    /// 
1265    /// # Arguments
1266    /// 
1267    /// * `pattern` - The command pattern from the rule
1268    /// * `command` - The requested command
1269    /// 
1270    /// # Returns
1271    /// 
1272    /// * `true` if the pattern matches the command
1273    /// * `false` otherwise
1274    fn command_matches(&self, pattern: &str, command: &str) -> bool {
1275        if pattern == "*" {
1276            return true; // Wildcard matches everything
1277        }
1278        
1279        if pattern == command {
1280            return true; // Exact match
1281        }
1282        
1283        // Check for wildcard suffix (e.g., "show *")
1284        if let Some(stripped) = pattern.strip_suffix('*') {
1285            let prefix = &stripped.trim();
1286            return command.starts_with(prefix);
1287        }
1288        
1289        false
1290    }
1291    
1292    
1293    /// Check if a rule matches an access request
1294    /// 
1295    /// This private helper function determines whether a specific rule
1296    /// applies to a given access request. A rule matches if ALL of its
1297    /// conditions are satisfied (AND logic).
1298    /// 
1299    /// # Matching Logic
1300    /// 
1301    /// * **Operations**: Rule must cover the requested operation
1302    /// * **Module**: Rule's module must match (or be unspecified)
1303    /// * **RPC**: Rule's RPC must match (or be wildcard/unspecified)
1304    /// * **Path**: Rule's path must match (with wildcard support)
1305    /// 
1306    /// # Arguments
1307    /// 
1308    /// * `rule` - The rule to check
1309    /// * `req` - The access request to match against
1310    /// 
1311    /// # Returns
1312    /// 
1313    /// * `true` if the rule matches the request
1314    /// * `false` if any condition fails
1315    fn rule_matches(&self, rule: &NacmRule, req: &AccessRequest) -> bool {
1316        // Check 1: Operations - Rule must cover the requested operation
1317        // If rule specifies operations, the request operation must be included
1318        if !rule.access_operations.is_empty() && !rule.access_operations.contains(&req.operation) {
1319            return false;  // Rule doesn't cover this operation
1320        }
1321        
1322        // Check 2: Context matching (Tail-f extension)
1323        if let Some(rule_context) = &rule.context {
1324            if let Some(req_context) = req.context {
1325                if !req_context.matches(rule_context) {
1326                    return false;  // Context doesn't match
1327                }
1328            } else if rule_context != "*" {
1329                // Rule specifies context but request has none
1330                return false;
1331            }
1332        }
1333        
1334        // Check 3: Module name matching
1335        // If rule specifies a module, request must be for the same module
1336        if let Some(rule_module) = &rule.module_name {
1337            if let Some(req_module) = req.module_name {
1338                if rule_module != req_module {
1339                    return false;  // Different modules
1340                }
1341            } else {
1342                return false;  // Rule requires module, but request has none
1343            }
1344        }
1345        
1346        // Check 4: RPC name matching
1347        // Special handling for wildcard ("*") RPCs
1348        if let Some(rule_rpc) = &rule.rpc_name {
1349            if rule_rpc == "*" {
1350                // Wildcard matches any RPC (or no RPC)
1351            } else if let Some(req_rpc) = req.rpc_name {
1352                if rule_rpc != req_rpc {
1353                    return false;  // Different RPC names
1354                }
1355            } else {
1356                return false;  // Rule requires specific RPC, but request has none
1357            }
1358        }
1359        
1360        // Check 5: Path matching (simplified XPath-style matching)
1361        // Supports exact matches and simple wildcard patterns
1362        if let Some(rule_path) = &rule.path {
1363            if rule_path == "/" {
1364                // Root path matches everything (universal path rule)
1365            } else if let Some(req_path) = req.path {
1366                if rule_path.ends_with("/*") {
1367                    // Wildcard path: "/interfaces/*" matches "/interfaces/interface[1]"
1368                    let prefix = &rule_path[..rule_path.len() - 2];
1369                    if !req_path.starts_with(prefix) {
1370                        return false;  // Path doesn't match prefix
1371                    }
1372                } else if rule_path != req_path {
1373                    return false;  // Exact path mismatch
1374                }
1375            } else {
1376                return false;  // Rule requires path, but request has none
1377            }
1378        }
1379        
1380        // All checks passed - rule matches this request
1381        true
1382    }
1383}
1384
1385// --- Example usage and tests ---
1386
1387#[cfg(test)]
1388mod tests {
1389    use super::*;
1390
1391    #[test]
1392    fn test_xml_parsing() {
1393        let xml = r#"
1394        <config xmlns="http://tail-f.com/ns/config/1.0">
1395            <nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
1396                <enable-nacm>true</enable-nacm>
1397                <read-default>deny</read-default>
1398                <write-default>deny</write-default>
1399                <exec-default>deny</exec-default>
1400                <groups>
1401                    <group>
1402                        <name>admin</name>
1403                        <user-name>admin</user-name>
1404                    </group>
1405                </groups>
1406                <rule-list>
1407                    <name>admin</name>
1408                    <group>admin</group>
1409                    <rule>
1410                        <name>any-rpc</name>
1411                        <rpc-name>*</rpc-name>
1412                        <access-operations>exec</access-operations>
1413                        <action>permit</action>
1414                    </rule>
1415                </rule-list>
1416            </nacm>
1417        </config>"#;
1418        
1419        let config = NacmConfig::from_xml(xml).unwrap();
1420        assert!(config.enable_nacm);
1421        assert_eq!(config.read_default, RuleEffect::Deny);
1422        assert_eq!(config.groups.len(), 1);
1423        assert_eq!(config.rule_lists.len(), 1);
1424    }
1425
1426    #[test]
1427    fn test_nacm_validation_admin() {
1428        let xml = r#"
1429        <config xmlns="http://tail-f.com/ns/config/1.0">
1430            <nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
1431                <enable-nacm>true</enable-nacm>
1432                <read-default>deny</read-default>
1433                <write-default>deny</write-default>
1434                <exec-default>deny</exec-default>
1435                <groups>
1436                    <group>
1437                        <name>admin</name>
1438                        <user-name>admin</user-name>
1439                    </group>
1440                </groups>
1441                <rule-list>
1442                    <name>admin</name>
1443                    <group>admin</group>
1444                    <rule>
1445                        <name>any-rpc</name>
1446                        <rpc-name>*</rpc-name>
1447                        <access-operations>exec</access-operations>
1448                        <action>permit</action>
1449                    </rule>
1450                </rule-list>
1451            </nacm>
1452        </config>"#;
1453        
1454        let config = NacmConfig::from_xml(xml).unwrap();
1455        
1456        let req = AccessRequest {
1457            user: "admin",
1458            module_name: None,
1459            rpc_name: Some("edit-config"),
1460            operation: Operation::Exec,
1461            path: None,
1462            context: Some(&RequestContext::NETCONF),
1463            command: None,
1464        };
1465        
1466        let result = config.validate(&req);
1467        assert_eq!(result.effect, RuleEffect::Permit);
1468    }
1469
1470    #[test]
1471    fn test_real_nacm_xml() {
1472        use std::path::Path;
1473        
1474        let xml_path = Path::new(env!("CARGO_MANIFEST_DIR"))
1475            .join("examples")
1476            .join("data")
1477            .join("aaa_ncm_init.xml");
1478            
1479        let xml = std::fs::read_to_string(&xml_path)
1480            .unwrap_or_else(|_| panic!("Failed to read XML file at {:?}", xml_path));
1481        
1482        let config = NacmConfig::from_xml(&xml).expect("Failed to parse XML");
1483        
1484        // Test admin user - should be able to execute any RPC
1485        let admin_req = AccessRequest {
1486            user: "admin",
1487            module_name: None,
1488            rpc_name: Some("edit-config"),
1489            operation: Operation::Exec,
1490            path: None,
1491            context: Some(&RequestContext::NETCONF),
1492            command: None,
1493        };
1494        let admin_result = config.validate(&admin_req);
1495        assert_eq!(admin_result.effect, RuleEffect::Permit);
1496        
1497        // Test oper user - should be denied edit-config
1498        let oper_req = AccessRequest {
1499            user: "oper",
1500            module_name: None,
1501            rpc_name: Some("edit-config"),
1502            operation: Operation::Exec,
1503            path: None,
1504            context: Some(&RequestContext::NETCONF),
1505            command: None,
1506        };
1507        let oper_result = config.validate(&oper_req);
1508        assert_eq!(oper_result.effect, RuleEffect::Deny);
1509        
1510        // Test oper user - should be denied writing to nacm module
1511        let nacm_write_req = AccessRequest {
1512            user: "oper",
1513            module_name: Some("ietf-netconf-acm"),
1514            rpc_name: None,
1515            operation: Operation::Update,
1516            path: Some("/"),
1517            context: Some(&RequestContext::NETCONF),
1518            command: None,
1519        };
1520        let nacm_write_result = config.validate(&nacm_write_req);
1521        assert_eq!(nacm_write_result.effect, RuleEffect::Deny);
1522        
1523        // Test any user with example module - should be permitted for /misc/*
1524        let example_req = AccessRequest {
1525            user: "Guest",
1526            module_name: Some("example"),
1527            rpc_name: None,
1528            operation: Operation::Read,
1529            path: Some("/misc/foo"),
1530            context: Some(&RequestContext::NETCONF),
1531            command: None,
1532        };
1533        let example_result = config.validate(&example_req);
1534        assert_eq!(example_result.effect, RuleEffect::Permit);
1535        
1536        // Test configuration loaded properly
1537        assert!(config.enable_nacm);
1538        assert_eq!(config.read_default, RuleEffect::Deny);
1539        assert_eq!(config.write_default, RuleEffect::Deny);
1540        assert_eq!(config.exec_default, RuleEffect::Deny);
1541        
1542        // Check groups
1543        assert_eq!(config.groups.len(), 2);
1544        assert!(config.groups.contains_key("admin"));
1545        assert!(config.groups.contains_key("oper"));
1546        
1547        let admin_group = &config.groups["admin"];
1548        assert_eq!(admin_group.users, vec!["admin", "private"]);
1549        
1550        let oper_group = &config.groups["oper"];
1551        assert_eq!(oper_group.users, vec!["oper", "public"]);
1552        
1553        // Check rule lists
1554        assert_eq!(config.rule_lists.len(), 3);
1555        
1556        println!("Successfully parsed {} groups and {} rule lists", 
1557                 config.groups.len(), config.rule_lists.len());
1558    }
1559    
1560    #[test]
1561    fn test_tailf_command_rules() {
1562        let xml = r#"
1563        <config xmlns="http://tail-f.com/ns/config/1.0">
1564            <nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
1565                <enable-nacm>true</enable-nacm>
1566                <read-default>deny</read-default>
1567                <write-default>deny</write-default>
1568                <exec-default>deny</exec-default>
1569                <cmd-read-default xmlns="http://tail-f.com/yang/acm">deny</cmd-read-default>
1570                <cmd-exec-default xmlns="http://tail-f.com/yang/acm">deny</cmd-exec-default>
1571                <log-if-default-permit xmlns="http://tail-f.com/yang/acm"/>
1572                <log-if-default-deny xmlns="http://tail-f.com/yang/acm"/>
1573                <groups>
1574                    <group>
1575                        <name>operators</name>
1576                        <user-name>oper</user-name>
1577                        <gid xmlns="http://tail-f.com/yang/acm">1000</gid>
1578                    </group>
1579                </groups>
1580                <rule-list>
1581                    <name>operators</name>
1582                    <group>operators</group>
1583                    <cmdrule xmlns="http://tail-f.com/yang/acm">
1584                        <name>cli-show-status</name>
1585                        <context>cli</context>
1586                        <command>show status</command>
1587                        <access-operations>read exec</access-operations>
1588                        <action>permit</action>
1589                        <log-if-permit/>
1590                    </cmdrule>
1591                    <cmdrule xmlns="http://tail-f.com/yang/acm">
1592                        <name>cli-help</name>
1593                        <context>cli</context>
1594                        <command>help</command>
1595                        <action>permit</action>
1596                    </cmdrule>
1597                    <cmdrule xmlns="http://tail-f.com/yang/acm">
1598                        <name>deny-reboot</name>
1599                        <context>*</context>
1600                        <command>reboot</command>
1601                        <action>deny</action>
1602                        <log-if-deny/>
1603                    </cmdrule>
1604                </rule-list>
1605            </nacm>
1606        </config>"#;
1607        
1608        let config = NacmConfig::from_xml(xml).unwrap();
1609        
1610        // Test Tail-f extensions parsed correctly
1611        assert_eq!(config.cmd_read_default, RuleEffect::Deny);
1612        assert_eq!(config.cmd_exec_default, RuleEffect::Deny);
1613        assert!(config.log_if_default_permit);
1614        assert!(config.log_if_default_deny);
1615        
1616        // Test group GID
1617        let oper_group = &config.groups["operators"];
1618        assert_eq!(oper_group.gid, Some(1000));
1619        
1620        // Test command rule parsed correctly
1621        let rule_list = &config.rule_lists[0];
1622        assert_eq!(rule_list.command_rules.len(), 3);
1623        
1624        let show_status_rule = &rule_list.command_rules[0];
1625        assert_eq!(show_status_rule.name, "cli-show-status");
1626        assert_eq!(show_status_rule.context.as_deref(), Some("cli"));
1627        assert_eq!(show_status_rule.command.as_deref(), Some("show status"));
1628        assert_eq!(show_status_rule.effect, RuleEffect::Permit);
1629        assert!(show_status_rule.log_if_permit);
1630        assert!(!show_status_rule.log_if_deny);
1631        
1632        // Test CLI command validation - should permit
1633        let cli_show_req = AccessRequest {
1634            user: "oper",
1635            module_name: None,
1636            rpc_name: None,
1637            operation: Operation::Read,
1638            path: None,
1639            context: Some(&RequestContext::CLI),
1640            command: Some("show status"),
1641        };
1642        let show_result = config.validate(&cli_show_req);
1643        assert_eq!(show_result.effect, RuleEffect::Permit);
1644        assert!(show_result.should_log); // Should log because rule has log-if-permit
1645        
1646        // Test CLI help command - should permit but not log
1647        let cli_help_req = AccessRequest {
1648            user: "oper",
1649            module_name: None,
1650            rpc_name: None,
1651            operation: Operation::Exec,
1652            path: None,
1653            context: Some(&RequestContext::CLI),
1654            command: Some("help"),
1655        };
1656        let help_result = config.validate(&cli_help_req);
1657        assert_eq!(help_result.effect, RuleEffect::Permit);
1658        assert!(!help_result.should_log); // No logging flags set
1659        
1660        // Test reboot command from any context - should deny and log
1661        let reboot_req = AccessRequest {
1662            user: "oper",
1663            module_name: None,
1664            rpc_name: None,
1665            operation: Operation::Exec,
1666            path: None,
1667            context: Some(&RequestContext::WebUI),
1668            command: Some("reboot"),
1669        };
1670        let reboot_result = config.validate(&reboot_req);
1671        assert_eq!(reboot_result.effect, RuleEffect::Deny);
1672        assert!(reboot_result.should_log); // Should log because rule has log-if-deny
1673        
1674        // Test command that doesn't match any rule - should use default and log
1675        let unknown_cmd_req = AccessRequest {
1676            user: "oper",
1677            module_name: None,
1678            rpc_name: None,
1679            operation: Operation::Exec,
1680            path: None,
1681            context: Some(&RequestContext::CLI),
1682            command: Some("unknown-command"),
1683        };
1684        let unknown_result = config.validate(&unknown_cmd_req);
1685        assert_eq!(unknown_result.effect, RuleEffect::Deny); // cmd-exec-default is deny
1686        assert!(unknown_result.should_log); // log-if-default-deny is true
1687    }
1688}