Skip to content

Commit 4f7983f

Browse files
authored
Cleaning up --list (#978)
2 parents 1cb8578 + 76a7429 commit 4f7983f

File tree

84 files changed

+503
-191
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+503
-191
lines changed

src/sudoers/ast.rs

-14
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,6 @@ pub enum Qualified<T> {
1818
}
1919

2020
impl<T> Qualified<T> {
21-
pub fn as_ref(&self) -> Qualified<&T> {
22-
match self {
23-
Qualified::Allow(item) => Qualified::Allow(item),
24-
Qualified::Forbid(item) => Qualified::Forbid(item),
25-
}
26-
}
27-
28-
pub fn negate(&self) -> Qualified<&T> {
29-
match self {
30-
Qualified::Allow(item) => Qualified::Forbid(item),
31-
Qualified::Forbid(item) => Qualified::Allow(item),
32-
}
33-
}
34-
3521
#[cfg(test)]
3622
pub fn as_allow(&self) -> Option<&T> {
3723
if let Self::Allow(v) = self {

src/sudoers/entry.rs

+74-13
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,72 @@ use crate::sudoers::{
44
ast::{Identifier, Qualified, UserSpecifier},
55
tokens::{ChDir, Meta},
66
};
7+
use crate::{
8+
common::{resolve::CurrentUser, SudoString},
9+
system::{interface::UserId, User},
10+
};
711

812
use self::verbose::Verbose;
913

1014
use super::{
11-
ast::{Authenticate, RunAs, Tag},
15+
ast::{Authenticate, Def, RunAs, Tag},
1216
tokens::Command,
1317
};
1418

1519
mod verbose;
1620

1721
pub struct Entry<'a> {
18-
run_as: &'a RunAs,
19-
cmd_specs: Vec<(Tag, Qualified<&'a Meta<Command>>)>,
22+
run_as: Option<&'a RunAs>,
23+
cmd_specs: Vec<(Tag, &'a Qualified<Meta<Command>>)>,
24+
cmd_alias: &'a [Def<Command>],
2025
}
2126

2227
impl<'a> Entry<'a> {
2328
pub(super) fn new(
24-
run_as: &'a RunAs,
25-
cmd_specs: Vec<(Tag, Qualified<&'a Meta<Command>>)>,
29+
run_as: Option<&'a RunAs>,
30+
cmd_specs: Vec<(Tag, &'a Qualified<Meta<Command>>)>,
31+
cmd_alias: &'a [Def<Command>],
2632
) -> Self {
2733
debug_assert!(!cmd_specs.is_empty());
2834

29-
Self { run_as, cmd_specs }
35+
Self {
36+
run_as,
37+
cmd_specs,
38+
cmd_alias,
39+
}
3040
}
3141

3242
pub fn verbose(self) -> impl fmt::Display + 'a {
3343
Verbose(self)
3444
}
3545
}
3646

47+
fn root_runas() -> RunAs {
48+
let name = User::from_uid(UserId::ROOT)
49+
.ok()
50+
.flatten()
51+
.map(|u| u.name)
52+
.unwrap_or(SudoString::new("root".into()).unwrap());
53+
54+
let name = UserSpecifier::User(Identifier::Name(name));
55+
let name = Qualified::Allow(Meta::Only(name));
56+
57+
RunAs {
58+
users: vec![name],
59+
groups: vec![],
60+
}
61+
}
62+
3763
impl fmt::Display for Entry<'_> {
3864
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39-
let Self { run_as, cmd_specs } = self;
65+
let Self {
66+
run_as,
67+
cmd_specs,
68+
cmd_alias,
69+
} = self;
70+
71+
let root_runas = root_runas();
72+
let run_as = run_as.unwrap_or(&root_runas);
4073

4174
f.write_str(" (")?;
4275
write_users(run_as, f)?;
@@ -56,7 +89,7 @@ impl fmt::Display for Entry<'_> {
5689

5790
write_tag(f, tag, last_tag)?;
5891
last_tag = Some(tag);
59-
write_spec(f, spec)?;
92+
write_spec(f, spec, cmd_alias, true, ", ")?;
6093
}
6194

6295
Ok(())
@@ -65,8 +98,10 @@ impl fmt::Display for Entry<'_> {
6598

6699
fn write_users(run_as: &RunAs, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
67100
if run_as.users.is_empty() {
68-
// XXX assumes that the superuser is called "root"
69-
f.write_str("root")?;
101+
match CurrentUser::resolve() {
102+
Ok(u) => f.write_str(&u.name)?,
103+
_ => f.write_str("?")?,
104+
};
70105
}
71106

72107
let mut is_first_user = true;
@@ -183,17 +218,29 @@ fn write_tag(f: &mut fmt::Formatter, tag: &Tag, last_tag: Option<&Tag>) -> fmt::
183218
Ok(())
184219
}
185220

186-
fn write_spec(f: &mut fmt::Formatter, spec: &Qualified<&Meta<Command>>) -> fmt::Result {
221+
fn write_spec(
222+
f: &mut fmt::Formatter,
223+
spec: &Qualified<Meta<Command>>,
224+
alias_list: &[Def<Command>],
225+
mut sign: bool,
226+
separator: &str,
227+
) -> fmt::Result {
187228
let meta = match spec {
188229
Qualified::Allow(meta) => meta,
189230
Qualified::Forbid(meta) => {
190-
f.write_str("!")?;
231+
sign = !sign;
191232
meta
192233
}
193234
};
194235

236+
match meta {
237+
Meta::All | Meta::Only(_) if !sign => f.write_str("!")?,
238+
_ => {}
239+
}
240+
195241
match meta {
196242
Meta::All => f.write_str("ALL")?,
243+
197244
Meta::Only((cmd, args)) => {
198245
write!(f, "{cmd}")?;
199246
if let Some(args) = args {
@@ -202,7 +249,21 @@ fn write_spec(f: &mut fmt::Formatter, spec: &Qualified<&Meta<Command>>) -> fmt::
202249
}
203250
}
204251
}
205-
Meta::Alias(alias) => f.write_str(alias)?,
252+
Meta::Alias(alias) => {
253+
// this will terminate, since AliasTable has been checked by sanitize_alias_table
254+
if let Some(Def(_, spec_list)) = alias_list.iter().find(|Def(id, _)| id == alias) {
255+
let mut is_first_iteration = true;
256+
for spec in spec_list {
257+
if !is_first_iteration {
258+
f.write_str(separator)?;
259+
}
260+
write_spec(f, spec, alias_list, sign, separator)?;
261+
is_first_iteration = false;
262+
}
263+
} else {
264+
f.write_str("???")?
265+
}
266+
}
206267
}
207268

208269
Ok(())

src/sudoers/entry/verbose.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ pub struct Verbose<'a>(pub Entry<'a>);
1111

1212
impl fmt::Display for Verbose<'_> {
1313
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14-
let Self(Entry { run_as, cmd_specs }) = self;
14+
let Self(Entry {
15+
run_as,
16+
cmd_specs,
17+
cmd_alias,
18+
}) = self;
19+
20+
let root_runas = super::root_runas();
21+
let run_as = run_as.unwrap_or(&root_runas);
1522

1623
let mut last_tag = None;
1724
for (tag, cmd_spec) in cmd_specs {
@@ -28,7 +35,7 @@ impl fmt::Display for Verbose<'_> {
2835
last_tag = Some(tag);
2936

3037
f.write_str("\n\t")?;
31-
super::write_spec(f, cmd_spec)?;
38+
super::write_spec(f, cmd_spec, cmd_alias, true, "\n\t")?;
3239
}
3340

3441
Ok(())

src/sudoers/mod.rs

+44-59
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ pub use policy::{AuthenticatingUser, Authentication, Authorization, DirChange, R
6767

6868
pub use self::entry::Entry;
6969

70+
type MatchedCommand<'a> = (Option<&'a RunAs>, (Tag, &'a Spec<Command>));
71+
7072
/// This function takes a file argument for a sudoers file and processes it.
7173
impl Sudoers {
7274
pub fn open(path: impl AsRef<Path>) -> Result<(Sudoers, Vec<Error>), io::Error> {
@@ -147,12 +149,11 @@ impl Sudoers {
147149
///
148150
/// the outer iterator are the `User_Spec`s; the inner iterator are the `Cmnd_Spec`s of
149151
/// said `User_Spec`s
150-
fn matching_user_specs<'a: 'b + 'c, 'b: 'c, 'c, User: UnixUser + PartialEq<User>>(
152+
fn matching_user_specs<'a, User: UnixUser + PartialEq<User>>(
151153
&'a self,
152-
invoking_user: &'b User,
153-
hostname: &'c system::Hostname,
154-
) -> impl Iterator<Item = impl Iterator<Item = (Option<&'a RunAs>, (Tag, &'a Spec<Command>))> + 'b>
155-
+ 'c {
154+
invoking_user: &'a User,
155+
hostname: &'a system::Hostname,
156+
) -> impl Iterator<Item = impl Iterator<Item = MatchedCommand<'a>>> {
156157
let Self { rules, aliases, .. } = self;
157158
let user_aliases = get_aliases(&aliases.user, &match_user(invoking_user));
158159
let host_aliases = get_aliases(&aliases.host, &match_token(hostname));
@@ -170,23 +171,14 @@ impl Sudoers {
170171
})
171172
}
172173

173-
/// returns `User_Spec`s that match `invoking_user` and `hostname` in a print-able format
174174
pub fn matching_entries<'a, User: UnixUser + PartialEq<User>>(
175175
&'a self,
176-
invoking_user: &User,
177-
hostname: &system::Hostname,
178-
) -> Vec<Entry<'a>> {
179-
// NOTE this method MUST NOT perform any filtering that `Self::check` does not do to
180-
// ensure `sudo $command` and `sudo --list` use the same permission checking logic
176+
invoking_user: &'a User,
177+
hostname: &'a system::Hostname,
178+
) -> impl Iterator<Item = Entry<'a>> {
181179
let user_specs = self.matching_user_specs(invoking_user, hostname);
182180

183-
let cmnd_aliases = unfold_alias_table(&self.aliases.cmnd);
184-
let mut entries = vec![];
185-
for cmd_specs in user_specs {
186-
group_cmd_specs_per_runas(cmd_specs, &mut entries, &cmnd_aliases);
187-
}
188-
189-
entries
181+
user_specs.flat_map(|cmd_specs| group_cmd_specs_per_runas(cmd_specs, &self.aliases.cmnd.1))
190182
}
191183

192184
pub(crate) fn solve_editor_path(&self) -> Option<PathBuf> {
@@ -214,51 +206,42 @@ impl Sudoers {
214206

215207
fn group_cmd_specs_per_runas<'a>(
216208
cmnd_specs: impl Iterator<Item = (Option<&'a RunAs>, (Tag, &'a Spec<Command>))>,
217-
entries: &mut Vec<Entry<'a>>,
218-
cmnd_aliases: &HashMap<&String, &'a Vec<Spec<Command>>>,
219-
) {
220-
static EMPTY_RUNAS: RunAs = RunAs {
221-
users: Vec::new(),
222-
groups: Vec::new(),
223-
};
224-
225-
let mut runas = None;
209+
cmnd_aliases: &'a [Def<Command>],
210+
) -> impl Iterator<Item = Entry<'a>> {
211+
let mut entries = vec![];
212+
let mut last_runas = None;
226213
let mut collected_specs = vec![];
227214

228-
for (new_runas, (tag, spec)) in cmnd_specs {
229-
if let Some(new_runas) = new_runas {
215+
// `distribute_tags` will have given every spec a reference to the "runas specification"
216+
// that applies to it. The output of sudo --list splits the CmndSpec list based on that:
217+
// every line only has a single "runas" specifier. So we need to combine them for that.
218+
//
219+
// But sudo --list also outputs lines that are from different lines in the sudoers file on
220+
// different lines in the output of sudo --list, so we cannot compare "by value". Luckily,
221+
// once a RunAs is parsed, it will have a unique identifier in the form of its address.
222+
let origin = |runas: Option<&RunAs>| runas.map(|r| r as *const _);
223+
224+
for (runas, (tag, spec)) in cmnd_specs {
225+
if origin(runas) != origin(last_runas) {
230226
if !collected_specs.is_empty() {
231227
entries.push(Entry::new(
232-
runas.take().unwrap_or(&EMPTY_RUNAS),
228+
last_runas,
233229
mem::take(&mut collected_specs),
230+
cmnd_aliases,
234231
));
235232
}
236233

237-
runas = Some(new_runas);
234+
last_runas = runas;
238235
}
239236

240-
let (negate, meta) = match spec {
241-
Qualified::Allow(meta) => (false, meta),
242-
Qualified::Forbid(meta) => (true, meta),
243-
};
244-
245-
if let Meta::Alias(alias_name) = meta {
246-
if let Some(specs) = cmnd_aliases.get(alias_name) {
247-
// expand Cmnd_Alias
248-
for spec in specs.iter() {
249-
let new_spec = if negate { spec.negate() } else { spec.as_ref() };
250-
251-
collected_specs.push((tag.clone(), new_spec))
252-
}
253-
}
254-
} else {
255-
collected_specs.push((tag, spec.as_ref()));
256-
}
237+
collected_specs.push((tag, spec));
257238
}
258239

259240
if !collected_specs.is_empty() {
260-
entries.push(Entry::new(runas.unwrap_or(&EMPTY_RUNAS), collected_specs));
241+
entries.push(Entry::new(last_runas, collected_specs, cmnd_aliases));
261242
}
243+
244+
entries.into_iter()
262245
}
263246

264247
fn read_sudoers<R: io::Read>(mut reader: R) -> io::Result<Vec<basic_parser::Parsed<Sudo>>> {
@@ -290,10 +273,18 @@ pub(super) struct AliasTable {
290273
}
291274

292275
/// A vector with a list defining the order in which it needs to be processed
293-
type VecOrd<T> = (Vec<usize>, Vec<T>);
276+
struct VecOrd<T>(Vec<usize>, Vec<T>);
277+
278+
impl<T> Default for VecOrd<T> {
279+
fn default() -> Self {
280+
VecOrd(Vec::default(), Vec::default())
281+
}
282+
}
294283

295-
fn elems<T>(vec: &VecOrd<T>) -> impl Iterator<Item = &T> {
296-
vec.0.iter().map(|&i| &vec.1[i])
284+
impl<T> VecOrd<T> {
285+
fn iter(&self) -> impl Iterator<Item = &T> {
286+
self.0.iter().map(|&i| &self.1[i])
287+
}
297288
}
298289

299290
/// Check if the user `am_user` is allowed to run `cmdline` on machine `on_host` as the requested
@@ -315,8 +306,6 @@ fn check_permission<User: UnixUser + PartialEq<User>, Group: UnixGroup>(
315306
let runas_user_aliases = get_aliases(&aliases.runas, &match_user(request.user));
316307
let runas_group_aliases = get_aliases(&aliases.runas, &match_group_alias(request.group));
317308

318-
// NOTE to ensure `sudo $command` and `sudo --list` behave the same, both this function and
319-
// `Sudoers::matching_entries` must call this `matching_user_specs` method
320309
let matching_user_specs = sudoers.matching_user_specs(am_user, on_host).flatten();
321310

322311
let allowed_commands = matching_user_specs.filter_map(|(runas, cmdspec)| {
@@ -490,10 +479,6 @@ fn match_command<'a>((cmd, args): (&'a Path, &'a [String])) -> (impl Fn(&Command
490479
}
491480
}
492481

493-
fn unfold_alias_table<T>(table: &VecOrd<Def<T>>) -> HashMap<&String, &Vec<Qualified<Meta<T>>>> {
494-
elems(table).map(|Def(id, list)| (id, list)).collect()
495-
}
496-
497482
/// Find all the aliases that a object is a member of; this requires [sanitize_alias_table] to have run first;
498483
/// I.e. this function should not be "pub".
499484
fn get_aliases<Predicate, T>(table: &VecOrd<Def<T>>, pred: &Predicate) -> FoundAliases
@@ -504,7 +489,7 @@ where
504489
let all = Qualified::Allow(Meta::All);
505490

506491
let mut set = HashMap::new();
507-
for Def(id, list) in elems(table) {
492+
for Def(id, list) in table.iter() {
508493
if find_item(list, &pred, &set).is_some() {
509494
set.insert(id.clone(), true);
510495
} else if find_item(once(&all).chain(list), &pred, &set).is_none() {

0 commit comments

Comments
 (0)