From f390f7fbae643a80ee37aa83b6aec584085fef42 Mon Sep 17 00:00:00 2001 From: Marc Schoolderman Date: Tue, 29 Oct 2024 14:22:04 +0100 Subject: [PATCH] add 'specific' scopes to Defaults entries (parsing only) --- src/sudoers/ast.rs | 40 ++++++++++++++++++++++++++++++++--- src/sudoers/ast_names.rs | 4 ++++ src/sudoers/mod.rs | 2 +- src/sudoers/test/mod.rs | 2 +- src/sudoers/tokens.rs | 45 +++++++++++++++++++++++++++++++++------- 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/sudoers/ast.rs b/src/sudoers/ast.rs index 4eb04f584..2b72642c4 100644 --- a/src/sudoers/ast.rs +++ b/src/sudoers/ast.rs @@ -115,7 +115,19 @@ pub enum Directive { HostAlias(Defs) = HARDENED_ENUM_VALUE_1, CmndAlias(Defs) = HARDENED_ENUM_VALUE_2, RunasAlias(Defs) = HARDENED_ENUM_VALUE_3, - Defaults(Vec<(String, ConfigValue)>) = HARDENED_ENUM_VALUE_4, + Defaults(Vec<(String, ConfigValue)>, ConfigScope) = HARDENED_ENUM_VALUE_4, +} + +/// AST object for the 'context' (host, user, cmnd, runas) of a Defaults directive +#[repr(u32)] +pub enum ConfigScope { + // "Defaults entries are parsed in the following order: + // generic, host and user Defaults first, then runas Defaults and finally command defaults." + Generic, + Host(SpecList), + User(SpecList), + RunAs(SpecList), + Command(SpecList), } pub type TextEnum = crate::defaults::StrEnum<'static>; @@ -499,7 +511,7 @@ impl Parse for Sudo { if let Some(users) = maybe(try_nonterminal::>(stream))? { // element 1 always exists (parse_list fails on an empty list) let key = &users[0]; - if let Some(directive) = maybe(get_directive(key, stream))? { + if let Some(directive) = maybe(get_directive(key, stream, start_pos))? { if users.len() != 1 { unrecoverable!(pos = start_pos, stream, "invalid user name list"); } @@ -582,6 +594,7 @@ impl Many for Def { fn get_directive( perhaps_keyword: &Spec, stream: &mut impl CharStream, + begin_pos: (usize, usize), ) -> Parsed { use super::ast::Directive::*; use super::ast::Meta::*; @@ -596,7 +609,28 @@ fn get_directive( "Host_Alias" => make(HostAlias(expect_nonterminal(stream)?)), "Cmnd_Alias" | "Cmd_Alias" => make(CmndAlias(expect_nonterminal(stream)?)), "Runas_Alias" => make(RunasAlias(expect_nonterminal(stream)?)), - "Defaults" => make(Defaults(expect_nonterminal(stream)?)), + "Defaults" => { + let allow_scope_modifier = stream.get_pos().0 == begin_pos.0 + && stream.get_pos().1 - begin_pos.1 == "Defaults".len(); + + let scope = if allow_scope_modifier { + if is_syntax('@', stream)? { + ConfigScope::Host(expect_nonterminal(stream)?) + } else if is_syntax(':', stream)? { + ConfigScope::User(expect_nonterminal(stream)?) + } else if is_syntax('!', stream)? { + ConfigScope::Command(expect_nonterminal(stream)?) + } else if is_syntax('>', stream)? { + ConfigScope::RunAs(expect_nonterminal(stream)?) + } else { + ConfigScope::Generic + } + } else { + ConfigScope::Generic + }; + + make(Defaults(expect_nonterminal(stream)?, scope)) + } _ => reject(), } } diff --git a/src/sudoers/ast_names.rs b/src/sudoers/ast_names.rs index e7a310d69..1b42db932 100644 --- a/src/sudoers/ast_names.rs +++ b/src/sudoers/ast_names.rs @@ -39,6 +39,10 @@ mod names { const DESCRIPTION: &'static str = "path to binary (or sudoedit)"; } + impl UserFriendly for tokens::SimpleCommand { + const DESCRIPTION: &'static str = "path to binary (or sudoedit)"; + } + impl UserFriendly for ( SpecList, diff --git a/src/sudoers/mod.rs b/src/sudoers/mod.rs index 2b82aefa3..1c04cad8a 100644 --- a/src/sudoers/mod.rs +++ b/src/sudoers/mod.rs @@ -642,7 +642,7 @@ fn analyze( Sudo::Decl(CmndAlias(mut def)) => cfg.aliases.cmnd.1.append(&mut def), Sudo::Decl(RunasAlias(mut def)) => cfg.aliases.runas.1.append(&mut def), - Sudo::Decl(Defaults(params)) => { + Sudo::Decl(Defaults(params, scope)) => { for (name, value) in params { set_default(cfg, name, value) } diff --git a/src/sudoers/test/mod.rs b/src/sudoers/test/mod.rs index 2990682fe..09e25ac7e 100644 --- a/src/sudoers/test/mod.rs +++ b/src/sudoers/test/mod.rs @@ -415,7 +415,7 @@ fn defaults_regression() { #[test] fn specific_defaults() { assert!(parse_line("Defaults !use_pty").is_decl()); - assert!(try_parse_line("Defaults!use_pty").is_none()); // this succeeds right now but should fail + assert!(try_parse_line("Defaults!use_pty").is_none()); assert!(parse_line("Defaults!/bin/bash !use_pty").is_decl()); assert!(try_parse_line("Defaults!/bin/bash!use_pty").is_none()); assert!(try_parse_line("Defaults !/bin/bash !use_pty").is_none()); diff --git a/src/sudoers/tokens.rs b/src/sudoers/tokens.rs index e5259814b..0e76bc1f6 100644 --- a/src/sudoers/tokens.rs +++ b/src/sudoers/tokens.rs @@ -152,21 +152,23 @@ impl Token for AliasName { /// A struct that represents valid command strings; this can contain escape sequences and are /// limited to 1024 characters. -pub type Command = (glob::Pattern, Option>); +pub type Command = (SimpleCommand, Option>); + +/// A type that is specific to 'only commands', that can only happen in "Defaults!command" contexts; +/// which is essentially a subset of "Command" +pub type SimpleCommand = glob::Pattern; impl Token for Command { const MAX_LEN: usize = 1024; fn construct(s: String) -> Result { - let cvt_err = |pat: Result<_, glob::PatternError>| { - pat.map_err(|err| format!("wildcard pattern error {err}")) - }; - // the tokenizer should not give us a token that consists of only whitespace let mut cmd_iter = s.split_whitespace(); - let mut cmd = cmd_iter.next().unwrap().to_string(); + let cmd = cmd_iter.next().unwrap().to_string(); let mut args = cmd_iter.map(String::from).collect::>(); + let command = SimpleCommand::construct(cmd)?; + let argpat = if args.is_empty() { // if no arguments are mentioned, anything is allowed None @@ -178,6 +180,32 @@ impl Token for Command { Some(args.into_boxed_slice()) }; + Ok((command, argpat)) + } + + // all commands start with "/" except "sudoedit" + fn accept_1st(c: char) -> bool { + SimpleCommand::accept_1st(c) + } + + fn accept(c: char) -> bool { + SimpleCommand::accept(c) || c == ' ' + } + + const ALLOW_ESCAPE: bool = SimpleCommand::ALLOW_ESCAPE; + fn escaped(c: char) -> bool { + SimpleCommand::escaped(c) + } +} + +impl Token for SimpleCommand { + const MAX_LEN: usize = 1024; + + fn construct(mut cmd: String) -> Result { + let cvt_err = |pat: Result<_, glob::PatternError>| { + pat.map_err(|err| format!("wildcard pattern error {err}")) + }; + // record if the cmd ends in a slash and remove it if it does let is_dir = cmd.ends_with('/') && { cmd.pop(); @@ -197,7 +225,7 @@ impl Token for Command { cmd.push_str("/*"); } - Ok((cvt_err(glob::Pattern::new(&cmd))?, argpat)) + cvt_err(glob::Pattern::new(&cmd)) } // all commands start with "/" except "sudoedit" @@ -211,11 +239,12 @@ impl Token for Command { const ALLOW_ESCAPE: bool = true; fn escaped(c: char) -> bool { - matches!(c, '\\' | ',' | ':' | '=' | '#') + matches!(c, '\\' | ',' | ':' | '=' | '#' | ' ') } } impl Many for Command {} +impl Many for SimpleCommand {} pub struct DefaultName(pub String);