pleaser/
lib.rs

1//    please
2//    Copyright (C) 2020-2021 ed neville
3//
4//    This program is free software: you can redistribute it and/or modify
5//    it under the terms of the GNU General Public License as published by
6//    the Free Software Foundation, either version 3 of the License, or
7//    (at your option) any later version.
8//
9//    This program is distributed in the hope that it will be useful,
10//    but WITHOUT ANY WARRANTY; without even the implied warranty of
11//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12//    GNU General Public License for more details.
13//
14//    You should have received a copy of the GNU General Public License
15//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17use regex::Regex;
18use std::collections::HashMap;
19use std::env;
20use std::ffi::{CStr, CString};
21use std::path::Path;
22use std::process;
23use syslog::{Facility, Formatter3164};
24
25use chrono::{NaiveDate, NaiveDateTime, Utc};
26use nix::sys::signal;
27use nix::sys::signal::*;
28
29use std::fmt;
30use std::fs;
31use std::fs::File;
32use std::io::prelude::*;
33use std::io::BufReader;
34use std::os::unix::io::AsRawFd;
35use std::time::SystemTime;
36use uzers::os::unix::UserExt;
37use uzers::*;
38
39use getopts::{Matches, Options};
40use nix::unistd::{alarm, gethostname, setegid, seteuid, setgid, setuid};
41use pam::Authenticator;
42
43use rand::distributions::Alphanumeric;
44use rand::{thread_rng, Rng};
45
46#[derive(PartialEq, Eq, Clone, Debug)]
47pub enum EditMode {
48    Mode(i32),
49    Keep(bool),
50}
51
52#[derive(PartialEq, Eq, Clone, Debug)]
53pub enum ReasonType {
54    Need(bool),
55    Text(String),
56}
57
58#[derive(Clone, Debug)]
59pub struct EnvOptions {
60    pub name: Option<String>,
61    pub exact_name: Option<String>,
62    pub rule: Option<String>,
63    pub exact_rule: Option<String>,
64    pub notbefore: Option<NaiveDateTime>,
65    pub notafter: Option<NaiveDateTime>,
66    pub datematch: Option<String>,
67    pub target: Option<String>,
68    pub exact_target: Option<String>,
69    pub target_group: Option<String>,
70    pub exact_target_group: Option<String>,
71    pub hostname: Option<String>,
72    pub exact_hostname: Option<String>,
73    pub permit: Option<bool>,
74    pub require_pass: Option<bool>,
75    pub acl_type: Acltype,
76    pub file_name: String,
77    pub section: String,
78    pub group: bool,
79    pub configured: bool,
80    pub dir: Option<String>,
81    pub exact_dir: Option<String>,
82    pub exitcmd: Option<String>,
83    pub edit_mode: Option<EditMode>,
84    pub reason: Option<ReasonType>,
85    pub last: Option<bool>,
86    pub syslog: Option<bool>,
87    pub env_permit: Option<String>,
88    pub env_assign: Option<HashMap<String, String>>,
89    pub timeout: Option<u32>,
90    pub search_path: Option<String>,
91    pub token_timeout: Option<u64>,
92}
93
94impl EnvOptions {
95    pub fn new() -> EnvOptions {
96        EnvOptions {
97            name: None,
98            exact_name: None,
99            rule: Some("^$".to_string()),
100            exact_rule: None,
101            target: Some("root".to_string()),
102            exact_target: None,
103            target_group: None,
104            exact_target_group: None,
105            notbefore: None,
106            notafter: None,
107            datematch: None,
108            hostname: None,
109            exact_hostname: None,
110            file_name: "".to_string(),
111            section: "".to_string(),
112            permit: None,
113            require_pass: None,
114            acl_type: Acltype::Run,
115            group: false,
116            configured: false,
117            dir: None,
118            exact_dir: None,
119            exitcmd: None,
120            edit_mode: None,
121            reason: None,
122            last: None,
123            syslog: None,
124            env_permit: None,
125            env_assign: None,
126            timeout: None,
127            search_path: None,
128            token_timeout: None,
129        }
130    }
131    fn new_deny() -> EnvOptions {
132        let mut opt = EnvOptions::new();
133        opt.permit = Some(false);
134        opt.rule = Some(".".to_string());
135        opt.target = Some("^$".to_string());
136        opt.acl_type = Acltype::List;
137        opt
138    }
139    pub fn permit(&self) -> bool {
140        if self.permit.is_some() && !self.permit.unwrap() {
141            return false;
142        }
143
144        true
145    }
146    pub fn require_pass(&self) -> bool {
147        if self.require_pass.is_some() && !self.require_pass.unwrap() {
148            return false;
149        }
150
151        true
152    }
153}
154
155impl Default for EnvOptions {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161#[derive(Clone, Debug)]
162pub struct RunOptions {
163    pub name: String,
164    pub original_uid: nix::unistd::Uid,
165    pub original_gid: nix::unistd::Gid,
166    pub target: String,
167    pub target_group: Option<String>,
168    pub command: String,
169    pub original_command: Vec<String>,
170    pub hostname: String,
171    pub directory: Option<String>,
172    pub groups: HashMap<String, u32>,
173    pub date: NaiveDateTime,
174    pub acl_type: Acltype,
175    pub reason: Option<String>,
176    pub syslog: bool,
177    pub prompt: bool,
178    pub purge_token: bool,
179    pub warm_token: bool,
180    pub new_args: Vec<String>,
181    pub old_umask: Option<nix::sys::stat::Mode>,
182    pub old_envs: Option<HashMap<String, String>>,
183    pub allow_env_list: Option<Vec<String>>,
184    pub env_options: Option<EnvOptions>,
185    pub cloned_args: Option<Vec<String>>,
186    pub located_bin: HashMap<String, Option<String>>,
187    pub resume: Option<bool>,
188}
189
190impl RunOptions {
191    pub fn new() -> RunOptions {
192        RunOptions {
193            name: "root".to_string(),
194            original_uid: nix::unistd::Uid::from_raw(get_current_uid()),
195            original_gid: nix::unistd::Gid::from_raw(get_current_gid()),
196            target: "".to_string(),
197            target_group: None,
198            command: "".to_string(),
199            original_command: vec![],
200            hostname: "localhost".to_string(),
201            date: Utc::now().naive_utc(),
202            groups: HashMap::new(),
203            directory: None,
204            acl_type: Acltype::Run,
205            reason: None,
206            syslog: true,
207            prompt: true,
208            purge_token: false,
209            warm_token: false,
210            new_args: vec![],
211            old_umask: None,
212            old_envs: None,
213            allow_env_list: None,
214            env_options: None,
215            cloned_args: None,
216            located_bin: HashMap::new(),
217            resume: None,
218        }
219    }
220}
221
222impl Default for RunOptions {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228struct PamConvo {
229    login: String,
230    passwd: Option<String>,
231    service: String,
232}
233
234impl pam::Converse for PamConvo {
235    fn prompt_echo(&mut self, _msg: &CStr) -> Result<CString, ()> {
236        CString::new(self.login.clone()).map_err(|_| ())
237    }
238    fn prompt_blind(&mut self, _msg: &CStr) -> Result<CString, ()> {
239        match rpassword::prompt_password(format!(
240            "[{}] password for {}: ",
241            self.service, self.login
242        )) {
243            Ok(password) => self.passwd = Some(password),
244            Err(_) => {
245                println!("Cannot read from terminal");
246                std::process::exit(1);
247            }
248        }
249
250        CString::new(self.passwd.clone().unwrap()).map_err(|_| ())
251    }
252    fn info(&mut self, _msg: &CStr) {}
253    fn error(&mut self, msg: &CStr) {
254        println!("[{} pam error] {}", self.service, msg.to_string_lossy());
255    }
256    fn username(&self) -> &str {
257        &self.login
258    }
259}
260
261#[derive(PartialEq, Eq, Clone, Debug)]
262pub enum Acltype {
263    Run,
264    List,
265    Edit,
266}
267
268impl fmt::Display for Acltype {
269    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
270        match *self {
271            Acltype::Run => write!(f, "run"),
272            Acltype::List => write!(f, "list"),
273            Acltype::Edit => write!(f, "edit"),
274        }
275    }
276}
277
278pub fn print_may_not(ro: &RunOptions) {
279    println!(
280        "You may not {} \"{}\" on {} as {}",
281        if ro.acl_type == Acltype::Run {
282            "execute".to_string()
283        } else {
284            ro.acl_type.to_string()
285        },
286        &ro.command,
287        &ro.hostname,
288        &ro.target
289    );
290}
291
292/// build a regex and replace %{USER} with the user str, prefix with ^ and suffix with $
293pub fn regex_build(
294    v: &str,
295    ro: &RunOptions,
296    config_path: &str,
297    section: &str,
298    line: Option<i32>,
299) -> Option<Regex> {
300    let rule = Regex::new(&format!(
301        "^{}$",
302        &v.replace("%{USER}", &ro.name)
303            .replace("%{HOSTNAME}", &ro.hostname)
304    ));
305    if rule.is_err() {
306        println!(
307            "Error parsing {}{}",
308            config_path,
309            if line.is_some() {
310                format!(": {}:{}", section, line.unwrap())
311            } else {
312                "".to_string()
313            }
314        );
315        return None;
316    }
317    Some(rule.unwrap())
318}
319
320/// return true if the inclusion exists and ends with .ini
321pub fn can_dir_include(file: &str) -> bool {
322    let p = Path::new(file);
323
324    if !p.is_file() {
325        return false;
326    }
327    can_include_file_pattern(file)
328}
329
330pub fn can_include_file_pattern(file: &str) -> bool {
331    let dir_pattern = Regex::new(r".*\.ini$").unwrap();
332
333    if dir_pattern.is_match(file) {
334        let p = Path::new(file);
335
336        if p.file_name().is_none() {
337            return false;
338        }
339
340        match p.file_name().unwrap().to_str() {
341            None => {
342                return false;
343            }
344            Some(f) => {
345                if f.starts_with('.') {
346                    return false;
347                }
348            }
349        }
350        return true;
351    }
352    false
353}
354
355/// print the usage
356pub fn print_usage(opts: &Options, header: &str) {
357    println!("usage:");
358    println!("{}", opts.usage(header));
359}
360
361/// added around easter time
362pub fn credits(service: &str) {
363    let mut contributors = [
364        "All of the Debian Rust Maintainers, and especially Sylvestre Ledru",
365        "Andy Kluger, for your feedback",
366        "Cyrus Wyett, jim was better than ed",
367        "@unmellow, for your early testing",
368        "noproto, for your detailed report",
369        "pin, for work with pkgsrc",
370        "Stanley Dziegiel, for ini suggestions",
371        "My wife and child, for putting up with me",
372        "The SUSE Security Team, especially Matthias Gerstner",
373    ];
374
375    print_version(service);
376
377    contributors.sort_unstable();
378
379    println!("\nWith thanks to the following teams and people, you got us where we are today.\n");
380    println!("If your name is missing, or incorrect, please get in contact.\n");
381    println!("In sort order:\n");
382
383    for i in contributors.iter() {
384        println!("\t{}", i);
385    }
386
387    println!("\nYou too of course, for motivating me.");
388    println!("\nI thank you all for your help.\n\n\t-- Edward Neville");
389}
390
391/// common opt arguments
392pub fn common_opt_arguments(
393    matches: &Matches,
394    opts: &Options,
395    ro: &mut RunOptions,
396    service: &str,
397    header: &str,
398) {
399    ro.new_args.clone_from(&matches.free);
400
401    if matches.opt_present("r") {
402        ro.reason = Some(matches.opt_str("r").unwrap());
403    }
404    if matches.opt_present("t") {
405        ro.target = matches.opt_str("t").unwrap();
406    }
407    if matches.opt_present("g") {
408        ro.target_group = Some(matches.opt_str("g").unwrap());
409    }
410    if matches.opt_present("u") {
411        ro.target = matches.opt_str("u").unwrap();
412    }
413
414    if matches.opt_str("u").is_some()
415        && matches.opt_str("t").is_some()
416        && matches.opt_str("t").unwrap() != matches.opt_str("u").unwrap()
417    {
418        println!("Cannot use -t and -u with conflicting values");
419        print_usage(opts, header);
420        std::process::exit(1);
421    }
422
423    if matches.opt_present("p") {
424        ro.purge_token = true;
425    }
426    if matches.opt_present("v") {
427        print_version(service);
428        std::process::exit(0);
429    }
430    if matches.opt_present("w") {
431        ro.warm_token = true;
432    }
433
434    if matches.opt_present("n") {
435        ro.prompt = false;
436    }
437
438    if matches.opt_present("h") {
439        if ro.new_args == ["credits"] {
440            credits(service);
441            std::process::exit(0);
442        }
443
444        print_usage(opts, header);
445        print_version(service);
446        std::process::exit(0);
447    }
448
449    if ro.purge_token {
450        if !esc_privs() {
451            std::process::exit(1);
452        }
453        remove_token(&ro.name);
454        if !drop_privs(ro) {
455            std::process::exit(1);
456        }
457        std::process::exit(0);
458    }
459
460    if ro.warm_token {
461        if ro.prompt {
462            challenge_password(ro, &EnvOptions::new(), service);
463        }
464        std::process::exit(0);
465    }
466
467    ro.hostname = gethostname()
468        .expect("Failed getting hostname")
469        .into_string()
470        .expect("Hostname wasn't valid UTF-8");
471}
472
473/// read an ini file and traverse includes
474pub fn read_ini(
475    conf: &str,
476    vec_eo: &mut Vec<EnvOptions>,
477    ro: &RunOptions,
478    fail_error: bool,
479    config_path: &str,
480    bytes: &mut u64,
481    ini_list: &mut HashMap<String, bool>,
482) -> bool {
483    let parse_datetime_from_str = NaiveDateTime::parse_from_str;
484    let parse_date_from_str = NaiveDate::parse_from_str;
485    let mut faulty = false;
486    let mut section = String::from("no section defined");
487    let mut in_section = false;
488    let mut opt = EnvOptions::new();
489
490    if ini_list.contains_key(config_path) {
491        println!("Error parsing already read file {}", config_path);
492        return false;
493    }
494
495    ini_list.insert(config_path.to_string(), true);
496
497    for (mut line_number, l) in conf.split('\n').enumerate() {
498        line_number += 1;
499        let line = l.trim();
500
501        if line.is_empty() || line.starts_with('#') {
502            continue;
503        }
504
505        if line.starts_with('[') && line.ends_with(']') {
506            in_section = true;
507            section = line[1..line.len() - 1].to_string();
508            if opt.configured {
509                vec_eo.push(opt);
510            }
511            opt = EnvOptions::new();
512            opt.section.clone_from(&section);
513            opt.file_name = String::from(config_path);
514            continue;
515        }
516
517        let equals_pos = line.find('=');
518        if equals_pos.is_none() {
519            continue;
520        }
521
522        let key = line[0..equals_pos.unwrap()].trim();
523        let value = line[equals_pos.unwrap() + 1..].trim();
524
525        if !in_section {
526            println!("Error parsing {}:{}", config_path, line_number);
527            faulty = true;
528            continue;
529        }
530
531        // env_assign is a special case as the key names are not known at compile time so do not fit in the match
532
533        if key.starts_with("env_assign.") {
534            let period_pos = key.find('.');
535            let env_name = key[period_pos.unwrap() + 1..].trim();
536            if !value.is_empty() {
537                if opt.clone().env_assign.is_none() {
538                    opt.env_assign = Some(HashMap::new());
539                }
540                opt.env_assign
541                    .as_mut()
542                    .unwrap()
543                    .entry(env_name.to_string())
544                    .or_insert_with(|| value.to_string());
545            }
546            continue;
547        }
548
549        match key {
550            "include" => {
551                if !value.starts_with('/') {
552                    println!("Includes should start with /");
553                    return true;
554                }
555                if read_ini_config_file(value, vec_eo, ro, fail_error, bytes, ini_list) {
556                    println!("Could not include file");
557                    return true;
558                }
559                continue;
560            }
561            "includedir" => {
562                if !value.starts_with('/') {
563                    println!("Includes should start with /");
564                    return true;
565                }
566                match fs::read_dir(value) {
567                    Err(_x) => {
568                        faulty = true;
569                    }
570                    Ok(inc) => {
571                        let mut collect = vec![];
572                        for file in inc {
573                            collect.push(file.unwrap().path().to_str().unwrap().to_string());
574                        }
575                        collect.sort();
576                        for file in collect {
577                            if !can_dir_include(&file) {
578                                continue;
579                            }
580                            if read_ini_config_file(&file, vec_eo, ro, fail_error, bytes, ini_list)
581                            {
582                                println!("Could not include file");
583                                return true;
584                            }
585                        }
586                    }
587                }
588
589                continue;
590            }
591            "name" => {
592                opt.name = Some(value.to_string());
593                opt.configured = true;
594                if fail_error
595                    && regex_build(value, ro, config_path, &section, Some(line_number as i32))
596                        .is_none()
597                {
598                    faulty = true;
599                }
600            }
601            "exact_name" => {
602                opt.exact_name = Some(value.to_string());
603                opt.configured = true;
604            }
605            "hostname" => {
606                opt.hostname = Some(value.to_string());
607                opt.configured = true;
608                if fail_error
609                    && regex_build(value, ro, config_path, &section, Some(line_number as i32))
610                        .is_none()
611                {
612                    faulty = true;
613                }
614            }
615            "exact_hostname" => {
616                opt.exact_hostname = Some(value.to_string());
617                opt.configured = true;
618            }
619            "target" => {
620                opt.target = Some(value.to_string());
621                if fail_error
622                    && regex_build(value, ro, config_path, &section, Some(line_number as i32))
623                        .is_none()
624                {
625                    faulty = true;
626                }
627            }
628            "exact_target" => {
629                opt.exact_target = Some(value.to_string());
630            }
631            "target_group" => {
632                opt.target_group = Some(value.to_string());
633                if fail_error
634                    && regex_build(value, ro, config_path, &section, Some(line_number as i32))
635                        .is_none()
636                {
637                    faulty = true;
638                }
639            }
640            "exact_target_group" => {
641                opt.exact_target_group = Some(value.to_string());
642            }
643            "permit" => opt.permit = Some(value == "true"),
644            "require_pass" => opt.require_pass = Some(value != "false"),
645            "type" => match value.to_lowercase().as_str() {
646                "edit" => opt.acl_type = Acltype::Edit,
647                "list" => opt.acl_type = Acltype::List,
648                _ => opt.acl_type = Acltype::Run,
649            },
650            "group" => opt.group = value == "true",
651            "regex" | "rule" => {
652                opt.rule = Some(value.to_string());
653                if fail_error
654                    && regex_build(value, ro, config_path, &section, Some(line_number as i32))
655                        .is_none()
656                {
657                    faulty = true;
658                }
659            }
660            "exact_regex" | "exact_rule" => {
661                opt.exact_rule = Some(value.to_string());
662                opt.configured = true;
663            }
664            "notbefore" if value.len() == 8 => {
665                opt.notbefore = Some(
666                    parse_date_from_str(value, "%Y%m%d")
667                        .unwrap()
668                        .and_hms_opt(0, 0, 0)
669                        .unwrap(),
670                )
671            }
672            "notafter" if value.len() == 8 => {
673                opt.notafter = Some(
674                    parse_date_from_str(value, "%Y%m%d")
675                        .unwrap()
676                        .and_hms_opt(23, 59, 59)
677                        .unwrap(),
678                )
679            }
680            "notbefore" if value.len() == 14 => {
681                opt.notbefore = Some(parse_datetime_from_str(value, "%Y%m%d%H%M%S").unwrap())
682            }
683            "notafter" if value.len() == 14 => {
684                opt.notafter = Some(parse_datetime_from_str(value, "%Y%m%d%H%M%S").unwrap())
685            }
686            "datematch" => {
687                opt.datematch = Some(value.to_string());
688                if fail_error
689                    && regex_build(value, ro, config_path, &section, Some(line_number as i32))
690                        .is_none()
691                {
692                    faulty = true;
693                }
694            }
695            "dir" => {
696                opt.dir = Some(value.to_string());
697                if fail_error
698                    && regex_build(value, ro, config_path, &section, Some(line_number as i32))
699                        .is_none()
700                {
701                    faulty = true;
702                }
703            }
704            "exact_dir" => {
705                opt.exact_dir = Some(value.to_string());
706                if fail_error
707                    && regex_build(value, ro, config_path, &section, Some(line_number as i32))
708                        .is_none()
709                {
710                    faulty = true;
711                }
712            }
713            "permit_env" => {
714                if !value.is_empty() {
715                    opt.env_permit = Some(value.to_string());
716                }
717            }
718            "exitcmd" => {
719                if !value.is_empty() {
720                    opt.exitcmd = Some(value.to_string());
721                }
722            }
723            "editmode" => {
724                if !value.is_empty() {
725                    if value.parse::<i16>().is_ok() {
726                        opt.edit_mode = Some(EditMode::Mode(
727                            i32::from_str_radix(value.trim_start_matches('0'), 8)
728                                .expect("unable to parse editmode"),
729                        ));
730                    } else if value.to_lowercase() == "keep" {
731                        opt.edit_mode = Some(EditMode::Keep(true));
732                    } else {
733                        println!("Could not convert {} to numerical file mode", value);
734                        faulty = true;
735                    }
736                }
737            }
738            "reason" => {
739                if value == "true" || value == "false" {
740                    opt.reason = Some(ReasonType::Need(value == "true"));
741                } else {
742                    opt.reason = Some(ReasonType::Text(value.to_string()));
743                }
744            }
745            "last" => opt.last = Some(value == "true"),
746            "syslog" => opt.syslog = Some(value == "true"),
747            "timeout" => {
748                let timeout: Result<u32, core::num::ParseIntError> = value.parse();
749                if fail_error && timeout.is_err() {
750                    faulty = true;
751                } else {
752                    opt.timeout = Some(timeout.unwrap());
753                }
754            }
755            "search_path" => {
756                opt.search_path = Some(value.to_string());
757            }
758            "token_timeout" => {
759                let token_timeout: Result<u64, core::num::ParseIntError> = value.parse();
760                if fail_error && token_timeout.is_err() {
761                    faulty = true;
762                } else {
763                    opt.token_timeout = Some(token_timeout.unwrap());
764                }
765            }
766            &_ => {
767                println!("Error parsing {}:{}", config_path, line_number);
768                faulty = true;
769            }
770        }
771    }
772
773    if opt.configured {
774        vec_eo.push(opt);
775    }
776
777    fail_error && faulty
778}
779
780/// read through an ini config file, appending EnvOptions to vec_eo
781/// hardcoded limit of 10M for confs
782pub fn read_ini_config_file(
783    config_path: &str,
784    vec_eo: &mut Vec<EnvOptions>,
785    ro: &RunOptions,
786    fail_error: bool,
787    bytes: &mut u64,
788    ini_list: &mut HashMap<String, bool>,
789) -> bool {
790    let path = Path::new(config_path);
791    let display = path.display();
792
793    let file = match File::open(path) {
794        Err(why) => {
795            println!("Could not open {}: {}", display, why);
796            return true;
797        }
798        Ok(file) => file,
799    };
800
801    match nix::sys::stat::fstat(file.as_raw_fd()) {
802        Err(why) => {
803            println!("Could not stat {}: {}", display, why);
804            return true;
805        }
806        Ok(stat_data) => {
807            if stat_data.st_mode & libc::S_IFREG != libc::S_IFREG {
808                println!("Refusing to open non-regular file");
809                return true;
810            }
811
812            if (stat_data.st_mode & !libc::S_IFMT) & (0o022) != 0 {
813                println!("Refusing to parse file as group or other write permission bits are set");
814                return true;
815            }
816        }
817    }
818
819    let byte_limit = 1024 * 1024 * 10;
820
821    if *bytes >= byte_limit {
822        println!("Exiting as too much config has already been read.");
823        std::process::exit(1);
824    }
825
826    let mut s = String::new();
827    let reader = BufReader::new(file).take(byte_limit).read_to_string(&mut s);
828
829    match reader {
830        Ok(n) => {
831            *bytes += s.as_bytes().len() as u64;
832            if n >= byte_limit as usize {
833                println!("Exiting as too much config has already been read.");
834                std::process::exit(1);
835            }
836        }
837        Err(why) => {
838            println!("Could not read {}: {}", display, why);
839            return true;
840        }
841    }
842
843    read_ini(&s, vec_eo, ro, fail_error, config_path, bytes, ini_list)
844}
845
846pub fn read_ini_config_str(
847    config: &str,
848    vec_eo: &mut Vec<EnvOptions>,
849    ro: &RunOptions,
850    fail_error: bool,
851    bytes: &mut u64,
852    ini_list: &mut HashMap<String, bool>,
853) -> bool {
854    read_ini(config, vec_eo, ro, fail_error, "static", bytes, ini_list)
855}
856
857/// may we execute with this hostname
858pub fn hostname_ok(item: &EnvOptions, ro: &RunOptions, line: Option<i32>) -> bool {
859    if item.exact_hostname.is_some() {
860        let hostname = item.exact_hostname.as_ref().unwrap();
861
862        if hostname != &ro.hostname
863            && hostname.ne(&"any".to_string())
864            && hostname.ne(&"localhost".to_string())
865        {
866            // println!("{}: hostname mismatch: {}", item.section, hostname);
867            return false;
868        }
869        return true;
870    }
871
872    if item.hostname.is_some() {
873        let hostname_re = match regex_build(
874            item.hostname.as_ref().unwrap(),
875            ro,
876            &item.file_name,
877            &item.section,
878            line,
879        ) {
880            Some(check) => check,
881            None => {
882                println!("Could not compile {}", &item.hostname.as_ref().unwrap());
883                return false;
884            }
885        };
886
887        if !hostname_re.is_match(&ro.hostname)
888            && !hostname_re.is_match("any")
889            && !hostname_re.is_match("localhost")
890        {
891            // println!("{}: hostname mismatch", item.section);
892            return false;
893        }
894    }
895    true
896}
897
898pub fn target_ok(item: &EnvOptions, ro: &RunOptions, line: Option<i32>) -> bool {
899    if item.exact_target.is_some() {
900        let exact_target = item.exact_target.as_ref().unwrap();
901        if exact_target == &ro.target {
902            return true;
903        }
904
905        // println!("{}: target mismatch: {} != {}", item.section, exact_target, ro.target);
906        return false;
907    }
908
909    if item.target.is_some() {
910        let target_re = match regex_build(
911            item.target.as_ref().unwrap(),
912            ro,
913            &item.file_name,
914            &item.section,
915            line,
916        ) {
917            Some(check) => check,
918            None => {
919                println!("Could not compile {}", &item.target.as_ref().unwrap());
920                return false;
921            }
922        };
923
924        if target_re.is_match(&ro.target) {
925            return true;
926        }
927        return false;
928    }
929    false
930}
931
932pub fn target_group_ok(item: &EnvOptions, ro: &RunOptions, line: Option<i32>) -> bool {
933    if (item.target_group.is_some() || item.exact_target_group.is_some())
934        && ro.target_group.is_none()
935    {
936        // println!("target_group is none");
937        return false;
938    }
939
940    if ro.target_group.is_none() {
941        // println!("target_group is none");
942        return true;
943    }
944
945    if item.exact_target_group.is_some() {
946        let exact_target_group = item.exact_target_group.as_ref().unwrap();
947        if exact_target_group == ro.target_group.as_ref().unwrap() {
948            return true;
949        }
950
951        // println!("{}: target group mismatch: {} != {}", item.section, exact_target_group, ro.target_group.as_ref().unwrap());
952        return false;
953    }
954
955    if item.target_group.is_some() {
956        let target_group_re = match regex_build(
957            item.target_group.as_ref().unwrap(),
958            ro,
959            &item.file_name,
960            &item.section,
961            line,
962        ) {
963            Some(check) => check,
964            None => {
965                println!("Could not compile {}", &item.target_group.as_ref().unwrap());
966                return false;
967            }
968        };
969
970        if target_group_re.is_match(ro.target_group.as_ref().unwrap()) {
971            return true;
972        }
973        return false;
974    }
975    false
976}
977
978pub fn rule_match(item: &EnvOptions, ro: &RunOptions, line: Option<i32>) -> bool {
979    if item.exact_rule.is_some() {
980        let exact_rule = item.exact_rule.as_ref().unwrap();
981        if exact_rule == &ro.command {
982            // println!("{}: exact rule match: {} == {}", item.section, exact_rule, ro.command);
983            return true;
984        }
985        // println!("{}: exact rule mismatch: {} != {}", item.section, exact_rule, ro.command);
986        return false;
987    }
988
989    if item.rule.is_some() {
990        let rule_re = match regex_build(
991            item.rule.as_ref().unwrap(),
992            ro,
993            &item.file_name,
994            &item.section,
995            line,
996        ) {
997            Some(check) => check,
998            None => {
999                println!("Could not compile {}", &item.rule.as_ref().unwrap());
1000                return false;
1001            }
1002        };
1003
1004        if rule_re.is_match(&ro.command) {
1005            // println!("{}: item rule is match", item.section);
1006            // opt = item.clone();
1007            return true;
1008        }
1009        // println!("{}: item rule is not match", &item.rule.as_ref().unwrap());
1010        return false;
1011    }
1012    false
1013}
1014
1015/// may we execute with this directory
1016pub fn directory_check_ok(item: &EnvOptions, ro: &RunOptions, line: Option<i32>) -> bool {
1017    if item.exact_dir.is_some() {
1018        if ro.directory.as_ref().is_none() {
1019            return false;
1020        }
1021
1022        let exact_dir = item.exact_dir.as_ref().unwrap();
1023
1024        if (ro.directory.as_ref()).is_some() && exact_dir != ro.directory.as_ref().unwrap() {
1025            return false;
1026        }
1027        return true;
1028    }
1029
1030    if item.dir.is_some() {
1031        if ro.directory.as_ref().is_none() {
1032            return false;
1033        }
1034
1035        let dir_re = match regex_build(
1036            item.dir.as_ref().unwrap(),
1037            ro,
1038            &item.file_name,
1039            &item.section,
1040            line,
1041        ) {
1042            Some(check) => check,
1043            None => {
1044                println!("Could not compile {}", &item.dir.as_ref().unwrap());
1045                return false;
1046            }
1047        };
1048
1049        if (ro.directory.as_ref()).is_some() && !dir_re.is_match(ro.directory.as_ref().unwrap()) {
1050            // && ro.directory != "." {
1051            return false;
1052        }
1053        return true;
1054    }
1055    if ro.directory.is_some() {
1056        return false;
1057    }
1058    true
1059}
1060
1061/// may we keep environment data
1062pub fn environment_ok(item: &EnvOptions, ro: &RunOptions, line: Option<i32>) -> bool {
1063    if ro.allow_env_list.is_none() {
1064        // println!("allow_env_list is none");
1065        return true;
1066    }
1067
1068    if item.env_permit.is_none() && ro.allow_env_list.is_some() {
1069        // println!("env_permit is none and allow_env_list is some");
1070        return false;
1071    }
1072
1073    let env_re = match regex_build(
1074        item.env_permit.as_ref().unwrap(),
1075        ro,
1076        &item.file_name,
1077        &item.section,
1078        line,
1079    ) {
1080        Some(check) => check,
1081        None => {
1082            println!("Could not compile {}", &item.env_permit.as_ref().unwrap());
1083            return false;
1084        }
1085    };
1086
1087    for permit_env in ro.allow_env_list.as_ref().unwrap() {
1088        // println!("permit_env is {}", permit_env);
1089        if !env_re.is_match(permit_env) {
1090            // println!( "{}: skipping as not a permitted env {} vs {}",    item.section, item.env_permit.clone().unwrap(), permit_env );
1091            return false;
1092        }
1093    }
1094
1095    true
1096}
1097
1098/// is the RunOption valid for the dates permitted in the EnvOption
1099pub fn permitted_dates_ok(item: &EnvOptions, ro: &RunOptions, line: Option<i32>) -> bool {
1100    if item.notbefore.is_some() && item.notbefore.unwrap() > ro.date {
1101        // println!("{}: now is before date", item.section);
1102        return false;
1103    }
1104
1105    if item.notafter.is_some() && item.notafter.unwrap() < ro.date {
1106        // println!("{}: now is after date", item.section);
1107        return false;
1108    }
1109
1110    if item.datematch.is_some() {
1111        let datematch_re = match regex_build(
1112            item.datematch.as_ref().unwrap(),
1113            ro,
1114            &item.file_name,
1115            &item.section,
1116            line,
1117        ) {
1118            Some(check) => check,
1119            None => {
1120                println!("Could not compile {}", &item.datematch.as_ref().unwrap());
1121                return false;
1122            }
1123        };
1124
1125        if !datematch_re.is_match(&ro.date.format("%a %e %b %T UTC %Y").to_string()) {
1126            // println!("{}: skipping as not a datematch {} vs {}", item.section, item.datematch.clone().unwrap(), &ro.date.format( "%a %e %b %T UTC %Y" ).to_string() );
1127            return false;
1128        }
1129    }
1130    true
1131}
1132
1133pub fn name_matches(item: &EnvOptions, ro: &RunOptions, line: Option<i32>) -> bool {
1134    if item.exact_name.is_some() {
1135        let name = item.exact_name.as_ref().unwrap();
1136        if name == &ro.name {
1137            return true;
1138        }
1139        //println!("{}: exact name mismatch: {} != {}", item.section, name, ro.name);
1140        return false;
1141    }
1142
1143    if item.name.is_some() {
1144        let name_re = match regex_build(
1145            item.name.as_ref().unwrap(),
1146            ro,
1147            &item.file_name,
1148            &item.section,
1149            line,
1150        ) {
1151            Some(check) => check,
1152            None => {
1153                println!("Could not compile {}", &item.name.as_ref().unwrap());
1154                return false;
1155            }
1156        };
1157
1158        if name_re.is_match(&ro.name) {
1159            // println!("{}: skipping as not a name match ({}), group={}", item.section, item.name.as_ref().unwrap(), item.group);
1160            return true;
1161        }
1162
1163        return false;
1164    }
1165    false
1166}
1167
1168pub fn group_matches(item: &EnvOptions, ro: &RunOptions, line: Option<i32>) -> bool {
1169    if item.exact_name.is_some() {
1170        let name = item.exact_name.as_ref().unwrap();
1171        for (k, _) in ro.groups.iter() {
1172            if name == k {
1173                // println!("{}: {} matches group {}", &item.section, name, k);
1174                return true;
1175            }
1176        }
1177        // println!("{}: does not match group", item.section);
1178        return false;
1179    }
1180
1181    if item.name.is_some() {
1182        let name_re = match regex_build(
1183            item.name.as_ref().unwrap(),
1184            ro,
1185            &item.file_name,
1186            &item.section,
1187            line,
1188        ) {
1189            Some(check) => check,
1190            None => {
1191                println!("Could not compile {}", &item.name.as_ref().unwrap());
1192                return false;
1193            }
1194        };
1195
1196        for (k, _) in ro.groups.iter() {
1197            if name_re.is_match(k) {
1198                // println!("{}: {} matches group {}", item.section, item.name.as_ref().unwrap(), k);
1199                return true;
1200            }
1201        }
1202        return false;
1203    }
1204
1205    false
1206}
1207
1208pub fn matching(item: &EnvOptions, ro: &mut RunOptions, line_error: Option<i32>) -> bool {
1209    if !permitted_dates_ok(item, ro, line_error) {
1210        // println!("Didn't match permitted dates");
1211        return false;
1212    }
1213
1214    if !item.group && !name_matches(item, ro, line_error) {
1215        // println!("not item group, and name does not match");
1216        return false;
1217    }
1218
1219    if item.group && !group_matches(item, ro, line_error) {
1220        // println!("item group, and group does not match");
1221        return false;
1222    }
1223
1224    if !hostname_ok(item, ro, line_error) {
1225        // println!("hostname does not match");
1226        return false;
1227    }
1228
1229    if !directory_check_ok(item, ro, line_error) {
1230        // println!("directory does not match");
1231        return false;
1232    }
1233
1234    if !environment_ok(item, ro, line_error) {
1235        // println!("environment does not match");
1236        return false;
1237    }
1238
1239    if !target_ok(item, ro, line_error) {
1240        // println!("target user does not match");
1241        return false;
1242    }
1243
1244    if !target_group_ok(item, ro, line_error) {
1245        // println!("target group does not match");
1246        return false;
1247    }
1248
1249    if item.acl_type == Acltype::List {
1250        // println!("{}: is list", item.section);
1251        return true;
1252    }
1253
1254    // cloned_args and command should be reset each loop
1255    // search_path could expose privilege paths that may appear
1256    // in error messaging
1257
1258    // shouldn't matter if setuid running user happened first, but even
1259    // so better than to be sorry later
1260    if ro.cloned_args.is_some() {
1261        ro.cloned_args = None;
1262    }
1263    ro.command = replace_new_args(ro.new_args.clone());
1264
1265    if item.acl_type == Acltype::Run {
1266        match search_path(ro, item) {
1267            None => {
1268                return false;
1269            }
1270            Some(x) => {
1271                ro.cloned_args = Some(ro.new_args.clone());
1272                ro.cloned_args.as_mut().unwrap()[0] = x;
1273                ro.command = replace_new_args(ro.cloned_args.as_ref().unwrap().clone());
1274            }
1275        }
1276    }
1277    if item.acl_type == Acltype::Edit {
1278        let edit_file = vec![ro.new_args[0].clone()];
1279        ro.command = replace_new_args(edit_file);
1280    }
1281
1282    rule_match(item, ro, line_error)
1283}
1284
1285pub fn merge_default(default: &EnvOptions, item: &EnvOptions) -> EnvOptions {
1286    let mut merged = item.clone();
1287
1288    if default.syslog.is_some() && item.syslog.is_none() {
1289        // println!("merging syslog");
1290        merged.syslog = default.syslog;
1291    }
1292
1293    if default.reason.is_some() && item.reason.is_none() {
1294        // println!("merging reason");
1295        merged.reason.clone_from(&default.reason);
1296    }
1297
1298    if default.require_pass.is_some() && item.require_pass.is_none() {
1299        // println!("merging require_pass");
1300        merged.require_pass = default.require_pass;
1301    }
1302
1303    if default.last.is_some() && item.last.is_none() {
1304        // println!("merging last");
1305        merged.last = default.last;
1306    }
1307
1308    if default.exitcmd.is_some() && item.exitcmd.is_none() {
1309        // println!("merging exitcmd");
1310        merged.exitcmd.clone_from(&default.exitcmd);
1311    }
1312
1313    if default.edit_mode.is_some() && item.edit_mode.is_none() {
1314        // println!("merging edit_mode");
1315        merged.edit_mode.clone_from(&default.edit_mode);
1316    }
1317
1318    if default.timeout.is_some() && item.timeout.is_none() {
1319        // println!("merging timeout");
1320        merged.timeout = default.timeout;
1321    }
1322
1323    if default.env_permit.is_some() && item.env_permit.is_none() {
1324        // println!("merging env_permit");
1325        merged.env_permit.clone_from(&default.env_permit);
1326    }
1327
1328    if default.env_assign.is_some() && item.env_assign.is_none() {
1329        // println!("merging env_assign");
1330        merged.env_assign.clone_from(&default.env_assign);
1331    }
1332
1333    if default.permit.is_some() && item.permit.is_none() {
1334        // println!("merging permit");
1335        merged.permit = default.permit;
1336    }
1337
1338    if default.search_path.is_some() && item.search_path.is_none() {
1339        // println!("merging search_path");
1340        merged.search_path.clone_from(&default.search_path);
1341    }
1342
1343    if default.token_timeout.is_some() && item.token_timeout.is_none() {
1344        // println!("merging token_timeout");
1345        merged.token_timeout = default.token_timeout;
1346    }
1347
1348    merged
1349}
1350
1351/// search the EnvOptions list for matching RunOptions and return the match
1352pub fn can(vec_eo: &[EnvOptions], ro: &mut RunOptions) -> EnvOptions {
1353    let mut opt = EnvOptions::new_deny();
1354    let mut default = EnvOptions::new();
1355
1356    for item in vec_eo {
1357        // println!("{}:", item.section);
1358        if item.acl_type != ro.acl_type {
1359            // println!("{}: not {:?} != {:?}", item.section, item.acl_type, ro.acl_type);
1360            continue;
1361        }
1362
1363        if !matching(item, ro, None) {
1364            // println!("!matching");
1365            continue;
1366        }
1367
1368        if item.section.starts_with("default") {
1369            default = merge_default(&default, item);
1370        }
1371
1372        opt = merge_default(&default, item);
1373
1374        match opt.last {
1375            None => {}
1376            Some(last) => {
1377                if last {
1378                    break;
1379                }
1380            }
1381        }
1382        // println!("didn't match");
1383    }
1384    opt
1385}
1386
1387/// check reason. this happens post authorize in order to provide feedback
1388pub fn reason_ok(item: &EnvOptions, ro: &RunOptions) -> bool {
1389    if item.reason.is_none() {
1390        return true;
1391    }
1392
1393    match &item.reason.as_ref().unwrap() {
1394        ReasonType::Text(value) => {
1395            let m_re = match regex_build(value, ro, &item.file_name, &item.section, None) {
1396                Some(check) => check,
1397                None => {
1398                    println!("Could not compile {}", &value);
1399                    return false;
1400                }
1401            };
1402
1403            if ro.reason.is_some() && m_re.is_match(ro.reason.as_ref().unwrap()) {
1404                return true;
1405            }
1406
1407            println!(
1408                "Sorry but there is no reason match to {} \"{}\" on {} as {}",
1409                &ro.acl_type, &ro.command, &ro.hostname, &ro.target
1410            );
1411
1412            false
1413        }
1414        ReasonType::Need(value) => {
1415            if value == &true && ro.reason.is_none() {
1416                println!(
1417                    "Sorry but no reason was given to {} \"{}\" on {} as {}",
1418                    &ro.acl_type,
1419                    if ro.acl_type == Acltype::List {
1420                        &ro.target
1421                    } else {
1422                        &ro.command
1423                    },
1424                    &ro.hostname,
1425                    &ro.target
1426                );
1427                return false;
1428            }
1429            true
1430        }
1431    }
1432}
1433
1434/// find editor for user. return /usr/bin/vi if EDITOR and VISUAL are unset
1435pub fn get_editor() -> String {
1436    let editor = "/usr/bin/vi";
1437
1438    for prog in [String::from("VISUAL"), String::from("EDITOR")].iter() {
1439        if let Ok(val) = std::env::var(prog) {
1440            return val;
1441        }
1442    }
1443
1444    editor.to_string()
1445}
1446
1447/// handler.authenticate without the root privs part for linux
1448#[cfg(target_os = "linux")]
1449pub fn handler_shim<T: pam::Converse>(
1450    _ro: &RunOptions,
1451    handler: &mut Authenticator<T>,
1452) -> Result<(), pam::PamError> {
1453    handler.authenticate()
1454}
1455
1456/// handler.authenticate needs esc_privs on netbsd
1457#[cfg(not(target_os = "linux"))]
1458pub fn handler_shim<T: pam::Converse>(
1459    ro: &RunOptions,
1460    handler: &mut Authenticator<T>,
1461) -> Result<(), pam::PamError> {
1462    if !esc_privs() {
1463        std::process::exit(1);
1464    }
1465    let auth = handler.authenticate();
1466    if !drop_privs(&ro) {
1467        std::process::exit(1);
1468    }
1469    auth
1470}
1471
1472/// read password of user via rpassword
1473/// should pam require a password, and it is successful, then we set a token
1474pub fn challenge_password(ro: &RunOptions, entry: &EnvOptions, service: &str) -> bool {
1475    if entry.require_pass() {
1476        if tty_name().is_none() {
1477            println!("Cannot read password without tty");
1478            return false;
1479        }
1480
1481        let mut retry_counter = 0;
1482
1483        if !esc_privs() {
1484            std::process::exit(1);
1485        }
1486
1487        if valid_token(&ro.name, entry) {
1488            update_token(&ro.name);
1489            return true;
1490        }
1491
1492        if !drop_privs(ro) {
1493            std::process::exit(1);
1494        }
1495
1496        if !ro.prompt {
1497            return false;
1498        }
1499
1500        let convo = PamConvo {
1501            login: ro.name.to_string(),
1502            passwd: None,
1503            service: service.to_string(),
1504        };
1505
1506        if entry.timeout.is_some() {
1507            extern "C" fn alarm_signal_handler(_: nix::libc::c_int) {
1508                println!("Timed out getting password");
1509
1510                let tty = std::fs::File::open("/dev/tty");
1511                if tty.is_ok() {
1512                    let term_res = nix::sys::termios::tcgetattr(tty.as_ref().unwrap());
1513                    if let Ok(mut term) = term_res {
1514                        term.local_flags
1515                            .set(nix::sys::termios::LocalFlags::ECHO, true);
1516
1517                        let res = nix::sys::termios::tcsetattr(
1518                            tty.as_ref().unwrap(),
1519                            nix::sys::termios::SetArg::TCSANOW,
1520                            &term,
1521                        );
1522                        if res.is_err() {
1523                            println!("Couldn't return terminal to original settings");
1524                        }
1525                    }
1526                }
1527
1528                std::process::exit(1);
1529            }
1530
1531            let sa = SigAction::new(
1532                SigHandler::Handler(alarm_signal_handler),
1533                SaFlags::SA_RESTART,
1534                SigSet::empty(),
1535            );
1536
1537            unsafe {
1538                match sigaction(Signal::SIGALRM, &sa) {
1539                    Ok(_) => {}
1540                    Err(_) => {
1541                        println!("Couldn't reset alarm");
1542                        std::process::exit(1);
1543                    }
1544                }
1545            }
1546        }
1547
1548        let mut handler = Authenticator::with_handler(service, convo).expect("Cannot init PAM");
1549
1550        loop {
1551            if let Some(timeout) = entry.timeout {
1552                alarm::set(timeout);
1553            }
1554
1555            let auth = handler_shim(ro, &mut handler);
1556
1557            if entry.timeout.is_some() {
1558                alarm::cancel();
1559            }
1560
1561            if auth.is_ok() {
1562                if handler.get_handler().passwd.is_some() {
1563                    unsafe { signal::signal(signal::SIGALRM, signal::SigHandler::SigDfl).unwrap() };
1564                    if !esc_privs() {
1565                        std::process::exit(1);
1566                    }
1567
1568                    update_token(&ro.name);
1569
1570                    if !drop_privs(ro) {
1571                        std::process::exit(1);
1572                    }
1573                }
1574                return true;
1575            }
1576            retry_counter += 1;
1577            if retry_counter == 3 {
1578                println!("Authentication failed :-(");
1579
1580                return false;
1581            }
1582        }
1583    }
1584    true
1585}
1586
1587/// return rule or exact_rule
1588pub fn list_rule(eo: &EnvOptions) -> String {
1589    if eo.exact_rule.is_some() {
1590        return format!("exact({})", eo.exact_rule.as_ref().unwrap());
1591    }
1592    if eo.rule.is_some() {
1593        return eo.rule.as_ref().unwrap().to_string();
1594    }
1595    "".to_string()
1596}
1597
1598/// return target or exact_target
1599pub fn list_target(eo: &EnvOptions) -> String {
1600    if eo.exact_target.is_some() {
1601        return format!("exact({})", eo.exact_target.as_ref().unwrap());
1602    }
1603    if eo.target.is_some() {
1604        return eo.target.as_ref().unwrap().to_string();
1605    }
1606    "".to_string()
1607}
1608
1609/// return dir or exact_dir
1610pub fn list_dir(eo: &EnvOptions) -> String {
1611    if eo.exact_dir.is_some() {
1612        return format!("exact({})", eo.exact_dir.as_ref().unwrap());
1613    }
1614    if eo.dir.is_some() {
1615        return eo.dir.as_ref().unwrap().to_string();
1616    }
1617    "".to_string()
1618}
1619
1620/// print output list of acl
1621pub fn list(vec_eo: &[EnvOptions], ro: &RunOptions) {
1622    //let mut str_list: vec![];
1623    for s in produce_list(vec_eo, ro) {
1624        println!("{}", s);
1625    }
1626}
1627
1628/// return EnvOptions as a vector of strings
1629pub fn produce_list(vec_eo: &[EnvOptions], ro: &RunOptions) -> Vec<String> {
1630    let mut str_list = vec![];
1631    let mut ro = ro.clone();
1632
1633    if !ro.target.is_empty() {
1634        ro.name.clone_from(&ro.target);
1635    }
1636
1637    let mut last_file = "";
1638
1639    for item in vec_eo {
1640        if !item.group && !name_matches(item, &ro, None) {
1641            continue;
1642        }
1643
1644        if item.group && !group_matches(item, &ro, None) {
1645            continue;
1646        }
1647
1648        let mut prefixes = vec![];
1649        if item.notbefore.is_some() && item.notbefore.unwrap() > ro.date {
1650            prefixes.push(format!("upcomming({})", item.notbefore.unwrap()));
1651        }
1652
1653        if item.notafter.is_some() && item.notafter.unwrap() < ro.date {
1654            prefixes.push(format!("expired({})", item.notafter.unwrap()));
1655        }
1656
1657        match &item.reason {
1658            Some(r) => {
1659                if *r != ReasonType::Need(false) {
1660                    prefixes.push(String::from("reason_required"));
1661                }
1662            }
1663            None => {}
1664        }
1665
1666        if item.acl_type != ro.acl_type {
1667            continue;
1668        }
1669
1670        if !item.permit() {
1671            prefixes.push(String::from("not permitted"));
1672        }
1673
1674        if !hostname_ok(item, &ro, None) {
1675            continue;
1676        }
1677
1678        if item.last.is_some() && item.last.unwrap() {
1679            prefixes.push(String::from("last"));
1680        }
1681
1682        let mut prefix = prefixes.join(", ");
1683        if !prefix.is_empty() {
1684            if item.acl_type != Acltype::List {
1685                prefix = format!(" {} as ", prefix);
1686            } else {
1687                prefix = format!(" {} to ", prefix);
1688            }
1689        }
1690        if last_file != item.file_name {
1691            str_list.push(format!("  in file: {}", item.file_name));
1692            last_file = &item.file_name;
1693        }
1694
1695        if item.acl_type == Acltype::List {
1696            str_list.push(format!(
1697                "    {}:{}list: {}",
1698                item.section,
1699                prefix,
1700                item.target.as_ref().unwrap()
1701            ));
1702            continue;
1703        }
1704
1705        str_list.push(format!(
1706            "    {}:{}{} (pass={},dirs={}): {}",
1707            item.section,
1708            prefix,
1709            list_target(item),
1710            item.require_pass(),
1711            list_dir(item),
1712            list_rule(item)
1713        ));
1714    }
1715    str_list
1716}
1717
1718/// return result from search cache lookup
1719pub fn search_path_cache(ro: &RunOptions, binary: &str) -> Option<String> {
1720    match ro.located_bin.get(binary) {
1721        Some(k) => {
1722            if k.is_none() {
1723                // println!("{} returning None (cached lookup)", item.section);
1724                return None;
1725            }
1726            // println!("{} returning Some({})", item.section, k.as_ref().unwrap().to_string());
1727            return Some(k.as_ref().unwrap().to_string());
1728        }
1729        None => {
1730            // println!("{} returning None (not a cached lookup)", item.section);
1731            None
1732        }
1733    }
1734}
1735
1736/// if binary is not an absolute/relative path, look for it in usual places
1737pub fn search_path(ro: &mut RunOptions, item: &EnvOptions) -> Option<String> {
1738    let binary = &ro.new_args[0];
1739    let p = Path::new(&binary);
1740    // println!("Searching for {binary}");
1741
1742    if binary.starts_with('/') || binary.starts_with("./") {
1743        let lookup = search_path_cache(ro, binary);
1744        if lookup.is_some() {
1745            return lookup;
1746        }
1747
1748        if !p.exists() {
1749            ro.located_bin.insert(binary.to_string(), None);
1750            return None;
1751        } else {
1752            ro.located_bin
1753                .insert(binary.to_string(), Some(binary.to_string()));
1754            return Some(binary.to_string());
1755        }
1756    }
1757
1758    let dirs = if item.search_path.is_some() {
1759        item.search_path.as_ref().unwrap()
1760    } else {
1761        "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
1762    };
1763
1764    for dir in dirs.split(':') {
1765        if dir.trim() == "" {
1766            continue;
1767        }
1768        let dir = dir.trim_end_matches('/');
1769        let path_name = format!("{}/{}", &dir, &binary);
1770
1771        if let Some(lookup) = search_path_cache(ro, binary) {
1772            return Some(lookup);
1773        }
1774
1775        let p = Path::new(&path_name);
1776
1777        if !p.exists() {
1778            ro.located_bin.insert(binary.to_string(), None);
1779            continue;
1780        }
1781
1782        // println!("inserting {binary} = {}", path_name.clone());
1783        ro.located_bin
1784            .insert(binary.to_string(), Some(path_name.clone()));
1785        return Some(path_name);
1786    }
1787
1788    None
1789}
1790
1791/// clean environment aside from ~half a dozen vars
1792pub fn clean_environment(ro: &mut RunOptions) {
1793    ro.old_umask = Some(nix::sys::stat::umask(
1794        nix::sys::stat::Mode::from_bits(0o077).unwrap(),
1795    ));
1796
1797    for (key, val) in std::env::vars() {
1798        if ro.acl_type == Acltype::Edit {
1799            if ro.old_envs.is_none() {
1800                ro.old_envs = Some(HashMap::new());
1801            }
1802
1803            ro.old_envs
1804                .as_mut()
1805                .unwrap()
1806                .entry(key.to_string())
1807                .or_insert(val);
1808        }
1809
1810        if key == "LANGUAGE"
1811            || key == "XAUTHORITY"
1812            || key == "LANG"
1813            || key == "LS_COLORS"
1814            || key == "TERM"
1815            || key == "DISPLAY"
1816            || key == "LOGNAME"
1817        {
1818            continue;
1819        }
1820
1821        let mut skip = false;
1822
1823        if ro.allow_env_list.is_some() {
1824            for env in ro.allow_env_list.as_ref().unwrap() {
1825                if key == *env {
1826                    skip = true;
1827                    break;
1828                }
1829            }
1830        }
1831        if skip {
1832            continue;
1833        }
1834
1835        if ro.acl_type == Acltype::Edit && (key == "EDITOR" || key == "VISUAL") {
1836            continue;
1837        }
1838        std::env::remove_var(key);
1839    }
1840}
1841
1842/// set the environment unless it is permitted to be kept and is specified
1843pub fn set_env_if_not_passed_through(ro: &RunOptions, key: &str, value: &str) {
1844    if ro.allow_env_list.is_some() {
1845        for env in ro.allow_env_list.as_ref().unwrap() {
1846            if key == *env {
1847                // println!("Returning as {} = {}", key, *env );
1848                return;
1849            }
1850        }
1851    }
1852
1853    std::env::set_var(key, value);
1854}
1855
1856/// set environment for helper scripts
1857pub fn set_environment(
1858    ro: &RunOptions,
1859    entry: &EnvOptions,
1860    original_user: &User,
1861    original_uid: u32,
1862    lookup_name: &User,
1863) {
1864    std::env::set_var("PLEASE_USER", original_user.name());
1865    std::env::set_var("PLEASE_UID", original_uid.to_string());
1866    std::env::set_var("PLEASE_GID", original_user.primary_group_id().to_string());
1867    std::env::set_var("PLEASE_COMMAND", &ro.command);
1868
1869    std::env::set_var("SUDO_USER", original_user.name());
1870    std::env::set_var("SUDO_UID", original_uid.to_string());
1871    std::env::set_var("SUDO_GID", original_user.primary_group_id().to_string());
1872    std::env::set_var("SUDO_COMMAND", &ro.command);
1873
1874    set_env_if_not_passed_through(
1875        ro,
1876        "PATH",
1877        "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
1878    );
1879
1880    set_env_if_not_passed_through(ro, "HOME", lookup_name.home_dir().to_str().unwrap());
1881    set_env_if_not_passed_through(ro, "MAIL", &format!("/var/mail/{}", ro.target));
1882    set_env_if_not_passed_through(ro, "SHELL", lookup_name.shell().to_str().unwrap());
1883    set_env_if_not_passed_through(ro, "USER", &ro.target);
1884    set_env_if_not_passed_through(ro, "LOGNAME", &ro.target);
1885
1886    if entry.env_assign.is_some() {
1887        for (k, v) in entry.env_assign.as_ref().unwrap() {
1888            std::env::set_var(k, v);
1889        }
1890    }
1891}
1892
1893pub fn bad_priv_msg() {
1894    println!("I cannot set privs. Exiting as not installed correctly.");
1895}
1896
1897/// set privs of usr to target_uid and target_gid. return false if fails
1898pub fn set_privs(user: &str, target_uid: nix::unistd::Uid, target_gid: nix::unistd::Gid) -> bool {
1899    let name = CString::new(user.as_bytes()).unwrap();
1900
1901    unsafe {
1902        if libc::setgroups(0, std::ptr::null()) != 0 {
1903            bad_priv_msg();
1904            return false;
1905        }
1906
1907        #[cfg(not(target_os = "macos"))]
1908        if libc::initgroups(name.as_ptr(), target_gid.as_raw()) != 0 {
1909            bad_priv_msg();
1910            return false;
1911        }
1912
1913        #[cfg(target_os = "macos")]
1914        if libc::initgroups(name.as_ptr(), target_gid.as_raw() as i32) != 0 {
1915            bad_priv_msg();
1916            return false;
1917        }
1918    }
1919
1920    if setgid(target_gid).is_err() {
1921        bad_priv_msg();
1922        return false;
1923    }
1924
1925    if setuid(target_uid).is_err() {
1926        bad_priv_msg();
1927        return false;
1928    }
1929    true
1930}
1931
1932/// set privs of usr to target_uid and target_gid. return false if fails
1933pub fn set_eprivs(target_uid: nix::unistd::Uid, target_gid: nix::unistd::Gid) -> bool {
1934    if setegid(target_gid).is_err() {
1935        bad_priv_msg();
1936        return false;
1937    }
1938    if seteuid(target_uid).is_err() {
1939        bad_priv_msg();
1940        return false;
1941    }
1942
1943    true
1944}
1945
1946/// set privs (just call eprivs based on ro)
1947pub fn drop_privs(ro: &RunOptions) -> bool {
1948    esc_privs() && set_eprivs(ro.original_uid, ro.original_gid)
1949}
1950
1951/// reset privs (just call eprivs based on root)
1952pub fn esc_privs() -> bool {
1953    set_eprivs(nix::unistd::Uid::from_raw(0), nix::unistd::Gid::from_raw(0))
1954}
1955
1956/// return our best guess of what the user's tty is
1957pub fn tty_name() -> Option<String> {
1958    let mut ttyname = None;
1959
1960    /* sometimes a tty isn't attached for all pipes FIXME: make this testable */
1961    for n in 0..3 {
1962        let ptr;
1963        unsafe {
1964            ptr = libc::ttyname(n);
1965        }
1966        if ptr.is_null() {
1967            continue;
1968        }
1969
1970        let s;
1971        unsafe {
1972            s = CStr::from_ptr(ptr).to_str();
1973        }
1974        match s {
1975            Err(_x) => ttyname = None,
1976            Ok(x) => ttyname = Some(x.to_string()),
1977        }
1978        break;
1979    }
1980
1981    ttyname
1982}
1983
1984/// add a level of escape to strings when they go to the old as " holds entities
1985pub fn escape_log(message: &str) -> String {
1986    message.replace('\"', "\\\"")
1987}
1988
1989/// write to syslog a standard log
1990pub fn log_action(service: &str, result: &str, ro: &RunOptions, command: &str) -> bool {
1991    if !ro.syslog {
1992        return false;
1993    }
1994
1995    let formatter = Formatter3164 {
1996        facility: Facility::LOG_USER,
1997        hostname: None,
1998        process: service.into(),
1999        pid: process::id(),
2000    };
2001
2002    let cwd = match env::current_dir() {
2003        Err(_) => "unable to get cwd".to_string(),
2004        Ok(x) => x.to_string_lossy().to_string(),
2005    };
2006
2007    let matching_env = match &ro.env_options {
2008        Some(env_options) => {
2009            format!("{}:{}", env_options.file_name, env_options.section)
2010        }
2011        None => "".to_string(),
2012    };
2013
2014    match syslog::unix(formatter) {
2015        Err(_e) => println!("Could not connect to syslog"),
2016        Ok(mut writer) => {
2017            let tty_name = tty_name();
2018
2019            writer
2020                .err(format!(
2021                    "user=\"{}\" cwd=\"{}\" tty=\"{}\" action=\"{}\" target=\"{}\" type=\"{}\" reason=\"{}\" command=\"{}\" matching_section=\"{}\"",
2022                    escape_log( &ro.name ),
2023                    escape_log( &cwd ),
2024                    if tty_name.is_none() {
2025                        "no_tty".to_string()
2026                    } else {
2027                        tty_name.unwrap()
2028                    },
2029                    result,
2030                    escape_log( &ro.target ),
2031                    ro.acl_type,
2032                    if ro.reason.as_ref().is_some() {
2033                        escape_log( ro.reason.as_ref().unwrap() )
2034                    } else {
2035                        String::from("")
2036                    },
2037                    escape_log( command ),
2038                    matching_env,
2039                ))
2040                .expect("could not write error message");
2041        }
2042    }
2043    false
2044}
2045
2046/// return the directory that the token should use
2047pub fn token_dir() -> String {
2048    "/var/run/please/token".to_string()
2049}
2050
2051/// return the path of the users token
2052pub fn token_path(user: &str) -> Option<String> {
2053    let tty_name = tty_name();
2054    tty_name.as_ref()?;
2055    let ppid = nix::unistd::getppid();
2056    Some(format!(
2057        "{}/{}:{}:{}",
2058        token_dir(),
2059        user,
2060        tty_name.unwrap().replace('/', "_"),
2061        ppid
2062    ))
2063}
2064
2065pub fn create_token_dir() -> bool {
2066    if !Path::new(&token_dir()).is_dir() && fs::create_dir_all(token_dir()).is_err() {
2067        println!("Could not create token directory");
2068        return false;
2069    }
2070
2071    true
2072}
2073
2074pub fn boot_secs() -> libc::timespec {
2075    let mut tp = libc::timespec {
2076        tv_sec: 0,
2077        tv_nsec: 0,
2078    };
2079    #[cfg(target_os = "linux")]
2080    unsafe {
2081        libc::clock_gettime(libc::CLOCK_BOOTTIME, &mut tp)
2082    };
2083    #[cfg(not(target_os = "linux"))]
2084    unsafe {
2085        libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut tp)
2086    };
2087    tp
2088}
2089
2090/// does the user have a valid token
2091/// return false if time stamp is in the future
2092/// return true if token was set within 600 seconds of wall and boot time
2093pub fn valid_token(user: &str, eo: &EnvOptions) -> bool {
2094    if !create_token_dir() {
2095        return false;
2096    }
2097
2098    let token_path = token_path(user);
2099    if token_path.is_none() {
2100        return false;
2101    }
2102
2103    let secs = eo.token_timeout.unwrap_or(600);
2104
2105    let token_path = token_path.unwrap();
2106    match fs::metadata(token_path) {
2107        Ok(meta) => {
2108            match meta.modified() {
2109                Ok(t) => {
2110                    let tp = boot_secs();
2111
2112                    match t.duration_since(SystemTime::UNIX_EPOCH) {
2113                        Ok(s) => {
2114                            if (tp.tv_sec as u64) < s.as_secs() {
2115                                // println!("tv_sec lower {} vs {}", tp.tv_sec, s.as_secs());
2116                                return false;
2117                            }
2118                            if ((tp.tv_sec as u64) - s.as_secs()) < secs {
2119                                // check the atime isn't older than 600 too
2120
2121                                match SystemTime::now().duration_since(meta.accessed().unwrap()) {
2122                                    Ok(a) => return a.as_secs() <= secs,
2123                                    Err(_) => return false,
2124                                }
2125                            }
2126                        }
2127                        Err(_) => {
2128                            return false;
2129                        }
2130                    }
2131
2132                    false
2133                }
2134                Err(_e) => false,
2135            }
2136        }
2137        Err(_) => false,
2138    }
2139}
2140
2141/// touch the users token on disk
2142pub fn update_token(user: &str) {
2143    if !create_token_dir() {
2144        return;
2145    }
2146
2147    let token_path = token_path(user);
2148    if token_path.is_none() {
2149        return;
2150    }
2151
2152    let old_mode = nix::sys::stat::umask(nix::sys::stat::Mode::from_bits(0o077).unwrap());
2153    let token_path = token_path.unwrap();
2154    let token_path_tmp = format!("{}.tmp", &token_path);
2155    match fs::File::create(&token_path_tmp) {
2156        Ok(_x) => {}
2157        Err(x) => println!("Error creating token: {}", x),
2158    }
2159    nix::sys::stat::umask(old_mode);
2160
2161    let tp = boot_secs();
2162
2163    let tv_mtime = nix::sys::time::TimeVal::from(libc::timeval {
2164        tv_sec: tp.tv_sec,
2165        tv_usec: 0,
2166    });
2167
2168    // https://github.com/rust-lang/libc/issues/1848
2169    #[cfg_attr(target_env = "musl", allow(deprecated))]
2170    let tv_atime = nix::sys::time::TimeVal::from(libc::timeval {
2171        tv_sec: SystemTime::now()
2172            .duration_since(SystemTime::UNIX_EPOCH)
2173            .unwrap()
2174            .as_secs() as libc::time_t,
2175        tv_usec: 0,
2176    });
2177
2178    if nix::sys::stat::utimes(Path::new(&token_path_tmp), &tv_atime, &tv_mtime).is_err() {
2179        return;
2180    }
2181
2182    if std::fs::rename(token_path_tmp.as_str(), token_path).is_err() {
2183        println!("Could not update token");
2184    }
2185}
2186
2187/// remove from disk the users token
2188pub fn remove_token(user: &str) {
2189    if !create_token_dir() {
2190        return;
2191    }
2192
2193    let token_location = token_path(user);
2194    if token_location.is_none() {
2195        return;
2196    }
2197
2198    let token_location = token_location.unwrap();
2199
2200    let p = Path::new(&token_location);
2201    if p.is_file() {
2202        match fs::remove_file(p) {
2203            Ok(_x) => {}
2204            Err(x) => println!("Error removing token {}: {}", p.to_str().unwrap(), x),
2205        }
2206    }
2207}
2208
2209/// turn group list into an indexed list
2210pub fn group_hash(groups: Vec<Group>) -> HashMap<String, u32> {
2211    let mut hm: HashMap<String, u32> = HashMap::new();
2212    for group in groups {
2213        hm.entry(String::from(group.name().to_string_lossy()))
2214            .or_insert_with(|| group.gid());
2215    }
2216    hm
2217}
2218
2219/// escape '\' within an argument
2220/// escape ' ' within an argument
2221pub fn replace_new_args(new_args: Vec<String>) -> String {
2222    let mut args = vec![];
2223    for arg in &new_args {
2224        args.push(arg.replace('\\', r"\\").replace(' ', r"\ "));
2225    }
2226
2227    args.join(" ")
2228}
2229
2230/// print version string
2231pub fn print_version(program: &str) {
2232    println!("{} version {}", &program, env!("CARGO_PKG_VERSION"));
2233}
2234
2235/// return a lump of random alpha numeric characters
2236pub fn prng_alpha_num_string(n: usize) -> String {
2237    let rng = thread_rng();
2238    rng.sample_iter(&Alphanumeric)
2239        .take(n)
2240        .map(|x| x as char)
2241        .collect()
2242}
2243
2244pub fn runopt_target_gid(ro: &RunOptions, lookup_name: &uzers::User) -> nix::unistd::Gid {
2245    if ro.target_group.is_some() {
2246        match nix::unistd::Group::from_name(ro.target_group.as_ref().unwrap()) {
2247            Ok(x) => match x {
2248                Some(g) => g.gid,
2249                None => {
2250                    println!("Cannot assign group {}", &ro.target_group.as_ref().unwrap());
2251                    std::process::exit(1);
2252                }
2253            },
2254            Err(_) => {
2255                println!("Cannot assign group {}", &ro.target_group.as_ref().unwrap());
2256                std::process::exit(1);
2257            }
2258        }
2259    } else {
2260        nix::unistd::Gid::from_raw(lookup_name.primary_group_id())
2261    }
2262}