Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specific Defaults #897

Merged
merged 9 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/defaults/settings_dsl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ macro_rules! modifier_of {
.ok()
.filter(|val| ($first ..= $last).contains(val))
.map(|i| {
Box::new(move |obj: &mut Settings| obj.$id = i) as Box<dyn FnOnce(&mut Settings)>
Box::new(move |obj: &mut Settings| obj.$id = i) as SettingsModifier
})
})
};
($id:ident, =int $fn: expr; $value: expr) => {
$crate::defaults::SettingKind::Integer(|text| {
$fn(&text).map(|i| {
Box::new(move |obj: &mut Settings| obj.$id = i) as Box<dyn FnOnce(&mut Settings)>
Box::new(move |obj: &mut Settings| obj.$id = i) as SettingsModifier
})
})
};
Expand Down
10 changes: 5 additions & 5 deletions src/sudo/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fn read_sudoers() -> Result<Sudoers, Error> {
Ok(sudoers)
}

fn judge(policy: Sudoers, context: &Context) -> Result<Judgement, Error> {
fn judge(mut policy: Sudoers, context: &Context) -> Result<Judgement, Error> {
Ok(policy.check(
&*context.current_user,
&context.hostname,
Expand All @@ -63,7 +63,7 @@ fn judge(policy: Sudoers, context: &Context) -> Result<Judgement, Error> {

impl<Auth: AuthPlugin> Pipeline<Auth> {
pub fn run(mut self, cmd_opts: SudoRunOptions) -> Result<(), Error> {
let policy = read_sudoers()?;
let mut policy = read_sudoers()?;

let (ctx_opts, pipe_opts) = cmd_opts.into();

Expand All @@ -73,7 +73,7 @@ impl<Auth: AuthPlugin> Pipeline<Auth> {
)
}

let mut context = Context::build_from_options(ctx_opts, policy.secure_path())?;
let mut context = Context::build_from_options(ctx_opts, policy.search_path())?;

let policy = judge(policy, &context)?;

Expand Down Expand Up @@ -136,8 +136,8 @@ impl<Auth: AuthPlugin> Pipeline<Auth> {
}

pub fn run_validate(mut self, cmd_opts: SudoValidateOptions) -> Result<(), Error> {
let policy = read_sudoers()?;
let mut context = Context::build_from_options(cmd_opts.into(), policy.secure_path())?;
let mut policy = read_sudoers()?;
let mut context = Context::build_from_options(cmd_opts.into(), policy.search_path())?;

match policy.validate_authorization() {
Authorization::Forbidden => {
Expand Down
8 changes: 4 additions & 4 deletions src/sudo/pipeline/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ impl<Auth: super::AuthPlugin> Pipeline<Auth> {

let original_command = cmd_opts.positional_args.first().cloned();

let sudoers = super::read_sudoers()?;
let mut sudoers = super::read_sudoers()?;

let mut context = Context::build_from_options(cmd_opts.into(), sudoers.secure_path())?;
let mut context = Context::build_from_options(cmd_opts.into(), sudoers.search_path())?;

if original_command.is_some() && !context.command.resolved {
return Err(Error::CommandNotFound(context.command.command));
Expand All @@ -43,7 +43,7 @@ impl<Auth: super::AuthPlugin> Pipeline<Auth> {
}

if let Some(original_command) = original_command {
check_sudo_command_perms(&original_command, &context, &other_user, &sudoers)?;
check_sudo_command_perms(&original_command, &context, &other_user, &mut sudoers)?;
} else {
let invoking_user = other_user.as_ref().unwrap_or(&context.current_user);
println_ignore_io_error!(
Expand Down Expand Up @@ -148,7 +148,7 @@ fn check_sudo_command_perms(
original_command: &str,
context: &Context,
other_user: &Option<User>,
sudoers: &Sudoers,
sudoers: &mut Sudoers,
) -> Result<(), Error> {
let user = other_user.as_ref().unwrap_or(&context.current_user);

Expand Down
63 changes: 60 additions & 3 deletions src/sudoers/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ impl<T> Qualified<T> {
pub type Spec<T> = Qualified<Meta<T>>;
pub type SpecList<T> = Vec<Spec<T>>;

/// A generic mapping function (only used for turning `Spec<SimpleCommand>` into `Spec<Command>`)
impl<T> Spec<T> {
pub fn map<U>(self, f: impl Fn(T) -> U) -> Spec<U> {
let transform = |meta| match meta {
Meta::All => Meta::All,
Meta::Alias(alias) => Meta::Alias(alias),
Meta::Only(x) => Meta::Only(f(x)),
};

match self {
Qualified::Allow(x) => Qualified::Allow(transform(x)),
Qualified::Forbid(x) => Qualified::Forbid(transform(x)),
}
}
}

/// An identifier is a name or a #number
#[cfg_attr(test, derive(Clone, Debug, PartialEq, Eq))]
#[repr(u32)]
Expand Down Expand Up @@ -120,7 +136,19 @@ pub enum Directive {
HostAlias(Defs<Hostname>) = HARDENED_ENUM_VALUE_1,
CmndAlias(Defs<Command>) = HARDENED_ENUM_VALUE_2,
RunasAlias(Defs<UserSpecifier>) = HARDENED_ENUM_VALUE_3,
Defaults(Vec<defaults::SettingsModifier>) = HARDENED_ENUM_VALUE_4,
Defaults(Vec<defaults::SettingsModifier>, ConfigScope) = HARDENED_ENUM_VALUE_4,
}

/// AST object for the 'context' (host, user, cmnd, runas) of a Defaults directive
#[repr(u32)]
squell marked this conversation as resolved.
Show resolved Hide resolved
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 = HARDENED_ENUM_VALUE_0,
Host(SpecList<Hostname>) = HARDENED_ENUM_VALUE_1,
User(SpecList<UserSpecifier>) = HARDENED_ENUM_VALUE_2,
RunAs(SpecList<UserSpecifier>) = HARDENED_ENUM_VALUE_3,
Command(SpecList<SimpleCommand>) = HARDENED_ENUM_VALUE_4,
}

/// The Sudoers file can contain permissions and directives
Expand Down Expand Up @@ -522,7 +550,7 @@ impl Parse for Sudo {
if let Some(users) = maybe(try_nonterminal::<SpecList<_>>(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");
}
Expand Down Expand Up @@ -612,9 +640,15 @@ impl<T> Many for Def<T> {
const SEP: char = ':';
}

// NOTE: This function is a bit of a hack, since it relies on the fact that all directives
// occur in the spot of a username, and are of a form that would otherwise be a legal user name.
// I.e. after a valid username has been parsed, we check if it isn't actually a valid start of a
// directive. A more robust solution would be to use the approach taken by the `MetaOrTag` above.

fn get_directive(
perhaps_keyword: &Spec<UserSpecifier>,
stream: &mut CharStream,
begin_pos: (usize, usize),
) -> Parsed<Directive> {
use super::ast::Directive::*;
use super::ast::Meta::*;
Expand All @@ -629,7 +663,30 @@ 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" => {
//HACK: this avoids having to add "Defaults@" etc as separate tokens; but relying
//on positional information during parsing is of course, cheating.
let allow_scope_modifier = stream.get_pos().0 == begin_pos.0
&& stream.get_pos().1 - begin_pos.1 == "Defaults".len();
squell marked this conversation as resolved.
Show resolved Hide resolved

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(),
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/sudoers/ast_names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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<tokens::Hostname>,
Expand Down
113 changes: 102 additions & 11 deletions src/sudoers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,20 @@ pub struct Error {
pub message: String,
}

/// A "Customiser" represents a "Defaults" setting that has 'late binding'; i.e.
/// cannot be determined simply by reading a sudoers configuration. This is used
/// for Defaults@host, Defaults:user, Defaults>runas and Defaults!cmd.
///
/// I.e. the Setting modifications in the second part of the tuple only apply for
/// items explicitly matched by the first part of the tuple.
type Customiser<T> = (SpecList<T>, Vec<defaults::SettingsModifier>);

#[derive(Default)]
pub struct Sudoers {
rules: Vec<PermissionSpec>,
aliases: AliasTable,
settings: Settings,
customisers: CustomiserTable,
squell marked this conversation as resolved.
Show resolved Hide resolved
}

/// A structure that represents what the user wants to do
Expand Down Expand Up @@ -84,12 +93,55 @@ impl Sudoers {
Ok(analyze(path.as_ref(), sudoers))
}

fn specify_host_user_runas<User: UnixUser + PartialEq<User>>(
&mut self,
hostname: &system::Hostname,
requesting_user: &User,
target_user: &User,
) {
let host_customisers = std::mem::take(&mut self.customisers.host);
let user_customisers = std::mem::take(&mut self.customisers.user);
specialise_setting(
&mut self.settings,
host_customisers,
&match_token(hostname),
&self.aliases.host,
);
specialise_setting(
&mut self.settings,
user_customisers,
&match_user(requesting_user),
&self.aliases.user,
);

let runas_customisers = std::mem::take(&mut self.customisers.runas);
specialise_setting(
&mut self.settings,
runas_customisers,
&match_user(target_user),
&self.aliases.runas,
);
}

fn specify_command(&mut self, command: &Path, arguments: &[String]) {
let customisers = std::mem::take(&mut self.customisers.cmnd);
specialise_setting(
&mut self.settings,
customisers,
&match_command((command, arguments)),
&self.aliases.cmnd,
);
}

pub fn check<User: UnixUser + PartialEq<User>, Group: UnixGroup>(
&self,
&mut self,
am_user: &User,
on_host: &system::Hostname,
request: Request<User, Group>,
) -> Judgement {
self.specify_host_user_runas(on_host, am_user, request.user);
self.specify_command(request.command, request.arguments);

// exception: if user is root or does not switch users, NOPASSWD is implied
let skip_passwd =
am_user.is_root() || (request.user == am_user && in_group(am_user, request.group));
Expand All @@ -103,7 +155,7 @@ impl Sudoers {

Judgement {
flags,
settings: self.settings.clone(), // this is wasteful, but in the future this will not be a simple clone and it avoids a lifetime
settings: self.settings.clone(),
}
}

Expand Down Expand Up @@ -264,14 +316,24 @@ fn open_subsudoers(path: &Path) -> io::Result<Vec<basic_parser::Parsed<Sudo>>> {
read_sudoers(source)
}

// note: trying to DRY using GAT's is tempting but doesn't make the code any shorter

#[derive(Default)]
pub(super) struct AliasTable {
struct AliasTable {
user: VecOrd<Def<UserSpecifier>>,
host: VecOrd<Def<tokens::Hostname>>,
host: VecOrd<Def<Hostname>>,
cmnd: VecOrd<Def<Command>>,
runas: VecOrd<Def<UserSpecifier>>,
}

#[derive(Default)]
struct CustomiserTable {
user: Vec<Customiser<UserSpecifier>>,
host: Vec<Customiser<Hostname>>,
cmnd: Vec<Customiser<Command>>,
runas: Vec<Customiser<UserSpecifier>>,
}

/// A vector with a list defining the order in which it needs to be processed
struct VecOrd<T>(Vec<usize>, Vec<T>);

Expand Down Expand Up @@ -399,7 +461,7 @@ where
}

/// A interface to access optional "satellite data"
trait WithInfo: Clone {
trait WithInfo {
type Item;
type Info;
fn as_inner(&self) -> Self::Item;
Expand Down Expand Up @@ -429,6 +491,23 @@ impl<'a> WithInfo for (Tag, &'a Spec<Command>) {
}
}

/// Apply a specialization to the Settings object, used for "specific" Defaults
fn specialise_setting<T>(
settings: &mut Settings,
customisers: impl IntoIterator<Item = Customiser<T>>,
matcher: &impl Fn(&T) -> bool,
alias_defs: &VecOrd<Def<T>>,
) {
let aliases = get_aliases(alias_defs, matcher);
for (list, modifiers) in customisers {
if find_item(&list, matcher, &aliases).is_some() {
for modifier in modifiers {
modifier(settings);
}
}
}
}

/// Now follow a collection of functions used as closures for `find_item`
fn match_user(user: &impl UnixUser) -> impl Fn(&UserSpecifier) -> bool + '_ {
move |spec| match spec {
Expand Down Expand Up @@ -585,16 +664,28 @@ fn analyze(

Sudo::Spec(permission) => cfg.rules.push(permission),

Sudo::Decl(UserAlias(mut def)) => cfg.aliases.user.1.append(&mut def),
Sudo::Decl(HostAlias(mut def)) => cfg.aliases.host.1.append(&mut def),
Sudo::Decl(CmndAlias(mut def)) => cfg.aliases.cmnd.1.append(&mut def),
Sudo::Decl(UserAlias(mut def)) => cfg.aliases.user.1.append(&mut def),
Sudo::Decl(RunasAlias(mut def)) => cfg.aliases.runas.1.append(&mut def),
Sudo::Decl(CmndAlias(mut def)) => cfg.aliases.cmnd.1.append(&mut def),

Sudo::Decl(Defaults(params)) => {
for modifier in params {
modifier(&mut cfg.settings);
Sudo::Decl(Defaults(params, scope)) => match scope {
ConfigScope::Generic => {
for modifier in params {
modifier(&mut cfg.settings)
}
}
}
ConfigScope::Host(specs) => cfg.customisers.host.push((specs, params)),
ConfigScope::User(specs) => cfg.customisers.user.push((specs, params)),
ConfigScope::RunAs(specs) => cfg.customisers.runas.push((specs, params)),
ConfigScope::Command(specs) => cfg.customisers.cmnd.push((
specs
.into_iter()
.map(|spec| spec.map(|simple_command| (simple_command, None)))
.collect(),
params,
)),
},

Sudo::Include(path, span) => include(
cfg,
Expand Down
2 changes: 1 addition & 1 deletion src/sudoers/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl Judgement {
}

impl Sudoers {
pub fn secure_path(&self) -> Option<&str> {
pub fn search_path(&mut self) -> Option<&str> {
self.settings.secure_path()
}

Expand Down
Loading
Loading