use super::ast_names::UserFriendly;
use super::basic_parser::*;
use super::char_stream::advance;
use super::tokens::*;
use crate::common::SudoString;
use crate::common::{
HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2, HARDENED_ENUM_VALUE_3,
HARDENED_ENUM_VALUE_4,
};
use crate::defaults;
#[cfg_attr(test, derive(Debug, Eq))]
#[derive(Clone, PartialEq)]
#[repr(u32)]
pub enum Qualified<T> {
Allow(T) = HARDENED_ENUM_VALUE_0,
Forbid(T) = HARDENED_ENUM_VALUE_1,
}
impl<T> Qualified<T> {
#[cfg(test)]
pub fn as_allow(&self) -> Option<&T> {
if let Self::Allow(v) = self {
Some(v)
} else {
None
}
}
}
pub type Spec<T> = Qualified<Meta<T>>;
pub type SpecList<T> = Vec<Spec<T>>;
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)),
}
}
}
#[cfg_attr(test, derive(Clone, Debug, PartialEq, Eq))]
#[repr(u32)]
pub enum Identifier {
Name(SudoString) = HARDENED_ENUM_VALUE_0,
ID(u32) = HARDENED_ENUM_VALUE_1,
}
#[cfg_attr(test, derive(Clone, Debug, PartialEq, Eq))]
#[repr(u32)]
pub enum UserSpecifier {
User(Identifier) = HARDENED_ENUM_VALUE_0,
Group(Identifier) = HARDENED_ENUM_VALUE_1,
NonunixGroup(Identifier) = HARDENED_ENUM_VALUE_2,
}
pub struct RunAs {
pub users: SpecList<UserSpecifier>,
pub groups: SpecList<Identifier>,
}
#[derive(Copy, Clone, Default, PartialEq)]
#[cfg_attr(test, derive(Debug, Eq))]
#[repr(u32)]
pub enum Authenticate {
#[default]
None = HARDENED_ENUM_VALUE_0,
Passwd = HARDENED_ENUM_VALUE_1,
Nopasswd = HARDENED_ENUM_VALUE_2,
}
#[derive(Copy, Clone, Default, PartialEq)]
#[cfg_attr(test, derive(Debug, Eq))]
#[repr(u32)]
pub enum EnvironmentControl {
#[default]
Implicit = HARDENED_ENUM_VALUE_0,
Setenv = HARDENED_ENUM_VALUE_1,
Nosetenv = HARDENED_ENUM_VALUE_2,
}
#[derive(Copy, Clone, Default, PartialEq)]
#[cfg_attr(test, derive(Debug, Eq))]
#[repr(u32)]
pub enum ExecControl {
#[default]
Implicit = HARDENED_ENUM_VALUE_0,
Exec = HARDENED_ENUM_VALUE_1,
Noexec = HARDENED_ENUM_VALUE_2,
}
#[derive(Default, Clone, PartialEq)]
#[cfg_attr(test, derive(Debug, Eq))]
pub struct Tag {
pub(super) authenticate: Authenticate,
pub(super) cwd: Option<ChDir>,
pub(super) env: EnvironmentControl,
pub(super) apparmor_profile: Option<String>,
pub(super) noexec: ExecControl,
}
impl Tag {
pub fn needs_passwd(&self) -> bool {
matches!(self.authenticate, Authenticate::None | Authenticate::Passwd)
}
}
pub struct CommandSpec(pub Vec<Modifier>, pub Spec<Command>);
type PairVec<A, B> = Vec<(A, Vec<B>)>;
pub struct PermissionSpec {
pub users: SpecList<UserSpecifier>,
pub permissions: PairVec<SpecList<Hostname>, (Option<RunAs>, CommandSpec)>,
}
pub type Defs<T> = Vec<Def<T>>;
pub struct Def<T>(pub String, pub SpecList<T>);
#[repr(u32)]
pub enum Directive {
UserAlias(Defs<UserSpecifier>) = HARDENED_ENUM_VALUE_0,
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>, ConfigScope) = HARDENED_ENUM_VALUE_4,
}
#[repr(u32)]
pub enum ConfigScope {
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,
}
#[repr(u32)]
pub enum Sudo {
Spec(PermissionSpec) = HARDENED_ENUM_VALUE_0,
Decl(Directive) = HARDENED_ENUM_VALUE_1,
Include(String, Span) = HARDENED_ENUM_VALUE_2,
IncludeDir(String, Span) = HARDENED_ENUM_VALUE_3,
LineComment = HARDENED_ENUM_VALUE_4,
}
impl Sudo {
#[cfg(test)]
pub fn is_spec(&self) -> bool {
matches!(self, Self::Spec(..))
}
#[cfg(test)]
pub fn is_decl(&self) -> bool {
matches!(self, Self::Decl(..))
}
#[cfg(test)]
pub fn is_line_comment(&self) -> bool {
matches!(self, Self::LineComment)
}
#[cfg(test)]
pub fn is_include(&self) -> bool {
matches!(self, Self::Include(..))
}
#[cfg(test)]
pub fn is_include_dir(&self) -> bool {
matches!(self, Self::IncludeDir(..))
}
#[cfg(test)]
pub fn as_include(&self) -> &str {
if let Self::Include(v, _) = self {
v
} else {
panic!()
}
}
#[cfg(test)]
pub fn as_spec(&self) -> Option<&PermissionSpec> {
if let Self::Spec(v) = self {
Some(v)
} else {
None
}
}
}
impl Parse for Identifier {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
if stream.eat_char('#') {
let Digits(guid) = expect_nonterminal(stream)?;
make(Identifier::ID(guid))
} else {
let Username(name) = try_nonterminal(stream)?;
make(Identifier::Name(name))
}
}
}
impl Many for Identifier {}
impl<T: Parse + UserFriendly> Parse for Qualified<T> {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
if is_syntax('!', stream)? {
let mut neg = true;
while is_syntax('!', stream)? {
neg = !neg;
}
let ident = expect_nonterminal(stream)?;
if neg {
make(Qualified::Forbid(ident))
} else {
make(Qualified::Allow(ident))
}
} else {
let ident = try_nonterminal(stream)?;
make(Qualified::Allow(ident))
}
}
}
impl<T: Many> Many for Qualified<T> {
const SEP: char = T::SEP;
const LIMIT: usize = T::LIMIT;
}
fn parse_meta<T: Parse>(
stream: &mut CharStream,
embed: impl FnOnce(SudoString) -> T,
) -> Parsed<Meta<T>> {
if let Some(meta) = try_nonterminal(stream)? {
make(match meta {
Meta::All => Meta::All,
Meta::Alias(alias) => Meta::Alias(alias),
Meta::Only(Username(name)) => Meta::Only(embed(name)),
})
} else {
make(Meta::Only(T::parse(stream)?))
}
}
impl Parse for Meta<Identifier> {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
parse_meta(stream, Identifier::Name)
}
}
impl Parse for UserSpecifier {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
fn parse_user(stream: &mut CharStream) -> Parsed<UserSpecifier> {
let userspec = if stream.eat_char('%') {
let ctor = if stream.eat_char(':') {
UserSpecifier::NonunixGroup
} else {
UserSpecifier::Group
};
ctor(expect_nonterminal(stream)?)
} else if stream.eat_char('+') {
unrecoverable!(stream, "netgroups are not supported yet");
} else {
UserSpecifier::User(try_nonterminal(stream)?)
};
make(userspec)
}
if stream.eat_char('"') {
let begin_pos = stream.get_pos();
let Unquoted(text, _): Unquoted<Username> = expect_nonterminal(stream)?;
let result = parse_user(&mut CharStream::new_with_pos(&text, begin_pos))?;
expect_syntax('"', stream)?;
Ok(result)
} else {
parse_user(stream)
}
}
}
impl Many for UserSpecifier {}
impl Parse for Meta<UserSpecifier> {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
parse_meta(stream, |name| UserSpecifier::User(Identifier::Name(name)))
}
}
impl Parse for RunAs {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
try_syntax('(', stream)?;
let users = try_nonterminal(stream).unwrap_or_default();
let groups = maybe(try_syntax(':', stream).and_then(|_| try_nonterminal(stream)))?
.unwrap_or_default();
expect_syntax(')', stream)?;
make(RunAs { users, groups })
}
}
struct MetaOrTag(Meta<Modifier>);
pub type Modifier = Box<dyn Fn(&mut Tag)>;
impl Parse for MetaOrTag {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
use Meta::*;
let start_pos = stream.get_pos();
let AliasName(keyword) = try_nonterminal(stream)?;
let mut switch = |modifier: fn(&mut Tag)| {
expect_syntax(':', stream)?;
make(Box::new(modifier))
};
let result: Modifier = match keyword.as_str() {
"INTERCEPT" => unrecoverable!(
pos = start_pos,
stream,
"INTERCEPT is not supported by sudo-rs"
),
"LOG_INPUT" | "NOLOG_INPUT" | "LOG_OUTPUT" | "NOLOG_OUTPUT" | "MAIL" | "NOMAIL"
| "FOLLOW" => {
eprintln_ignore_io_error!(
"warning: {} tags are ignored by sudo-rs",
keyword.as_str()
);
switch(|_| {})?
}
"NOFOLLOW" | "NOINTERCEPT" => switch(|_| {})?,
"EXEC" => switch(|tag| tag.noexec = ExecControl::Exec)?,
"NOEXEC" => switch(|tag| tag.noexec = ExecControl::Noexec)?,
"SETENV" => switch(|tag| tag.env = EnvironmentControl::Setenv)?,
"NOSETENV" => switch(|tag| tag.env = EnvironmentControl::Nosetenv)?,
"PASSWD" => switch(|tag| tag.authenticate = Authenticate::Passwd)?,
"NOPASSWD" => switch(|tag| tag.authenticate = Authenticate::Nopasswd)?,
"CWD" => {
expect_syntax('=', stream)?;
let path: ChDir = expect_nonterminal(stream)?;
Box::new(move |tag| tag.cwd = Some(path.clone()))
}
spec @ ("CHROOT" | "TIMEOUT" | "NOTBEFORE" | "NOTAFTER") => unrecoverable!(
pos = start_pos,
stream,
"{spec} is not supported by sudo-rs"
),
"ROLE" | "TYPE" => unrecoverable!(
pos = start_pos,
stream,
"SELinux role based access control is not yet supported by sudo-rs"
),
"APPARMOR_PROFILE" => {
expect_syntax('=', stream)?;
let StringParameter(profile) = expect_nonterminal(stream)?;
Box::new(move |tag| tag.apparmor_profile = Some(profile.clone()))
}
"ALL" => return make(MetaOrTag(All)),
alias => {
if is_syntax('=', stream)? {
unrecoverable!(pos = start_pos, stream, "unsupported modifier '{}'", alias);
} else {
return make(MetaOrTag(Alias(alias.to_string())));
}
}
};
make(MetaOrTag(Only(result)))
}
}
impl Parse for CommandSpec {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
use Qualified::Allow;
let mut tags = vec![];
while let Some(MetaOrTag(keyword)) = try_nonterminal(stream)? {
match keyword {
Meta::Only(modifier) => tags.push(modifier),
Meta::All => return make(CommandSpec(tags, Allow(Meta::All))),
Meta::Alias(name) => return make(CommandSpec(tags, Allow(Meta::Alias(name)))),
}
if tags.len() > Identifier::LIMIT {
unrecoverable!(stream, "too many tags for command specifier")
}
}
let cmd: Spec<Command> = expect_nonterminal(stream)?;
make(CommandSpec(tags, cmd))
}
}
impl Parse for (SpecList<Hostname>, Vec<(Option<RunAs>, CommandSpec)>) {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
let hosts = try_nonterminal(stream)?;
expect_syntax('=', stream)?;
let runas_cmds = expect_nonterminal(stream)?;
make((hosts, runas_cmds))
}
}
impl Many for (SpecList<Hostname>, Vec<(Option<RunAs>, CommandSpec)>) {
const SEP: char = ':';
}
impl Parse for (Option<RunAs>, CommandSpec) {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
let runas: Option<RunAs> = try_nonterminal(stream)?;
let cmd = if runas.is_some() {
expect_nonterminal(stream)?
} else {
try_nonterminal(stream)?
};
make((runas, cmd))
}
}
impl Many for (Option<RunAs>, CommandSpec) {}
impl Parse for Sudo {
fn parse(stream: &mut CharStream) -> Parsed<Sudo> {
if stream.eat_char('@') {
return parse_include(stream);
}
if stream.peek() == Some('#') {
return if let Ok(ident) = try_nonterminal::<Identifier>(stream) {
let first_user = Qualified::Allow(Meta::Only(UserSpecifier::User(ident)));
let users = if is_syntax(',', stream)? {
let mut rest = expect_nonterminal::<SpecList<_>>(stream)?;
rest.insert(0, first_user);
rest
} else {
vec![first_user]
};
let permissions = expect_nonterminal(stream)?;
make(Sudo::Spec(PermissionSpec { users, permissions }))
} else {
parse_include(stream).or_else(|_| {
stream.skip_to_newline();
make(Sudo::LineComment)
})
};
}
let start_pos = stream.get_pos();
if stream.peek() == Some('"') {
let users = expect_nonterminal(stream)?;
let permissions = expect_nonterminal(stream)?;
make(Sudo::Spec(PermissionSpec { users, permissions }))
} else if let Some(users) = maybe(try_nonterminal::<SpecList<_>>(stream))? {
let key = &users[0];
if let Some(directive) = maybe(get_directive(key, stream, start_pos))? {
if users.len() != 1 {
unrecoverable!(pos = start_pos, stream, "invalid user name list");
}
make(Sudo::Decl(directive))
} else {
let permissions = expect_nonterminal(stream)?;
make(Sudo::Spec(PermissionSpec { users, permissions }))
}
} else {
make(Sudo::LineComment)
}
}
}
fn parse_include(stream: &mut CharStream) -> Parsed<Sudo> {
fn get_path(stream: &mut CharStream, key_pos: (usize, usize)) -> Parsed<(String, Span)> {
let path = if stream.eat_char('"') {
let QuotedIncludePath(path) = expect_nonterminal(stream)?;
expect_syntax('"', stream)?;
path
} else {
let value_pos = stream.get_pos();
let IncludePath(path) = expect_nonterminal(stream)?;
if stream.peek() != Some('\n') {
unrecoverable!(
pos = value_pos,
stream,
"use quotes around filenames or escape whitespace"
)
}
path
};
make((
path,
Span {
start: key_pos,
end: stream.get_pos(),
},
))
}
let key_pos = stream.get_pos();
let result = match try_nonterminal(stream)? {
Some(Username(key)) if key == "include" => {
let (path, span) = get_path(stream, key_pos)?;
Sudo::Include(path, span)
}
Some(Username(key)) if key == "includedir" => {
let (path, span) = get_path(stream, key_pos)?;
Sudo::IncludeDir(path, span)
}
_ => unrecoverable!(pos = key_pos, stream, "unknown directive"),
};
make(result)
}
impl<T> Parse for Def<T>
where
T: UserFriendly,
Meta<T>: Parse + Many,
{
fn parse(stream: &mut CharStream) -> Parsed<Self> {
let begin_pos = stream.get_pos();
let AliasName(name) = try_nonterminal(stream)?;
if name == "ALL" {
unrecoverable!(
pos = begin_pos,
stream,
"the reserved alias ALL cannot be redefined"
);
}
expect_syntax('=', stream)?;
make(Def(name, expect_nonterminal(stream)?))
}
}
impl<T> Many for Def<T> {
const SEP: char = ':';
}
fn get_directive(
perhaps_keyword: &Spec<UserSpecifier>,
stream: &mut CharStream,
begin_pos: (usize, usize),
) -> Parsed<Directive> {
use super::ast::Directive::*;
use super::ast::Meta::*;
use super::ast::Qualified::*;
use super::ast::UserSpecifier::*;
let Allow(Only(User(Identifier::Name(keyword)))) = perhaps_keyword else {
return reject();
};
match keyword.as_str() {
"User_Alias" => make(UserAlias(expect_nonterminal(stream)?)),
"Host_Alias" => make(HostAlias(expect_nonterminal(stream)?)),
"Cmnd_Alias" | "Cmd_Alias" => make(CmndAlias(expect_nonterminal(stream)?)),
"Runas_Alias" => make(RunasAlias(expect_nonterminal(stream)?)),
_ if keyword.starts_with("Defaults") => {
const DEFAULTS_LEN: usize = "Defaults".len();
let allow_scope_modifier = stream.get_pos().0 == begin_pos.0
&& (stream.get_pos().1 - begin_pos.1 == DEFAULTS_LEN
|| keyword.len() > DEFAULTS_LEN);
let scope = if allow_scope_modifier {
if keyword[DEFAULTS_LEN..].starts_with('@') {
let inner_stream = &mut CharStream::new_with_pos(
&keyword[DEFAULTS_LEN + 1..],
advance(begin_pos, DEFAULTS_LEN + 1),
);
ConfigScope::Host(expect_nonterminal(inner_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(),
}
}
impl Parse for defaults::SettingsModifier {
fn parse(stream: &mut CharStream) -> Parsed<Self> {
let id_pos = stream.get_pos();
let parse_vars = |stream: &mut CharStream| -> Parsed<Vec<String>> {
if stream.eat_char('"') {
let mut result = Vec::new();
while let Some(EnvVar(name)) = try_nonterminal(stream)? {
if is_syntax('=', stream)? {
let StringParameter(value) = expect_nonterminal(stream)?;
result.push(name + "=" + &value);
} else {
result.push(name);
}
if result.len() > Identifier::LIMIT {
unrecoverable!(stream, "environment variable list too long")
}
}
expect_syntax('"', stream)?;
if result.is_empty() {
unrecoverable!(stream, "empty string not allowed");
}
make(result)
} else {
let EnvVar(name) = expect_nonterminal(stream)?;
if is_syntax('=', stream)? {
unrecoverable!(stream, "double quotes are required for VAR=value pairs")
} else {
make(vec![name])
}
}
};
let list_items =
|mode: defaults::ListMode, name: String, cfg: defaults::SettingKind, stream: &mut _| {
expect_syntax('=', stream)?;
let defaults::SettingKind::List(checker) = cfg else {
unrecoverable!(pos = id_pos, stream, "{name} is not a list parameter");
};
make(checker(mode, parse_vars(stream)?))
};
let text_item = |stream: &mut CharStream| {
if stream.eat_char('"') {
let QuotedStringParameter(text) = expect_nonterminal(stream)?;
expect_syntax('"', stream)?;
make(text)
} else {
let StringParameter(name) = expect_nonterminal(stream)?;
make(name)
}
};
if is_syntax('!', stream)? {
let value_pos = stream.get_pos();
let DefaultName(name) = expect_nonterminal(stream)?;
let Some(modifier) = defaults::negate(&name) else {
if defaults::set(&name).is_some() {
unrecoverable!(
pos = value_pos,
stream,
"'{name}' cannot be used in a boolean context"
);
} else {
unrecoverable!(pos = value_pos, stream, "unknown setting: '{name}'");
}
};
make(modifier)
} else {
let DefaultName(name) = try_nonterminal(stream)?;
let Some(cfg) = defaults::set(&name) else {
unrecoverable!(pos = id_pos, stream, "unknown setting: '{name}'");
};
if is_syntax('+', stream)? {
list_items(defaults::ListMode::Add, name, cfg, stream)
} else if is_syntax('-', stream)? {
list_items(defaults::ListMode::Del, name, cfg, stream)
} else if is_syntax('=', stream)? {
let value_pos = stream.get_pos();
match cfg {
defaults::SettingKind::Flag(_) => {
unrecoverable!(stream, "can't assign to boolean setting '{name}'")
}
defaults::SettingKind::Integer(checker) => {
let Numeric(denotation) = expect_nonterminal(stream)?;
if let Some(modifier) = checker(&denotation) {
make(modifier)
} else {
unrecoverable!(
pos = value_pos,
stream,
"'{denotation}' is not a valid value for {name}"
);
}
}
defaults::SettingKind::List(checker) => {
let items = parse_vars(stream)?;
make(checker(defaults::ListMode::Set, items))
}
defaults::SettingKind::Text(checker) => {
let text = text_item(stream)?;
let Some(modifier) = checker(&text) else {
unrecoverable!(
pos = value_pos,
stream,
"'{text}' is not a valid value for {name}"
);
};
make(modifier)
}
}
} else {
let defaults::SettingKind::Flag(modifier) = cfg else {
unrecoverable!(pos = id_pos, stream, "'{name}' is not a boolean setting");
};
make(modifier)
}
}
}
}
impl Many for defaults::SettingsModifier {}