diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f606ae18..5fd11bdf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,7 @@ jobs: run: | sudo ./dependencies.sh -yd sudo ./configure.sh -yd + sudo sed -i 's/"immutable": true/"immutable": false/g' /etc/security/rootasrole.json echo "/home/runner/.cargo/bin" >> $GITHUB_PATH - name: Configure PAM run: | @@ -38,8 +39,12 @@ jobs: - name: getenv run: env - name: Install Project + env: + PROFILE: debug run: sudo -E make -e install - name: Run Sr + env: + RUST_LOG: debug run: /usr/bin/sr -h - name: Run Chsr with sr run: sr /usr/bin/chsr -h diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6707a390..8caaeeaf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,19 +11,18 @@ on: jobs: rust-coverage: runs-on: ubuntu-latest + container: + image: xd009642/tarpaulin:develop-nightly + options: --security-opt seccomp=unconfined steps: - name: Checkout code uses: actions/checkout@v2 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - components: llvm-tools-preview - override: true - - - name: Install grcov - run: cargo install grcov + + - name: Update apt-get + run: apt update -y + + - name: Install sudo + run: apt install sudo -y - name: Install Dependencies run: sudo ./dependencies.sh -yd @@ -32,21 +31,12 @@ jobs: run: sudo ./configure.sh -yd - name: run tests with coverage - run: cargo test - env: - RUST_LOG: debug - CARGO_INCREMENTAL: 0 - RUSTFLAGS: '-Cinstrument-coverage' - LLVM_PROFILE_FILE: 'cargo-test-%p-%m.profraw' - continue-on-error: true - - - name: generate report - run: grcov . --binary-path ./target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/debug/rootasrole.lcov - + run: cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --bin chsr --bin sr --exclude-files capable* capable-ebpf/src/vmlinux.rs capable/src/main.rs build.rs --out Xml + - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: - file: target/debug/rootasrole.lcov + file: cobertura.xml flags: unittests diff --git a/.gitignore b/.gitignore index 889094bb..2a43ffa8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.ll *.pyc *.profraw +*.info # Linker output *.ilk @@ -66,3 +67,7 @@ bin/ # Rust crates Cargo.lock + +# Html results +*.html +*.xml \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index bf4f2c22..b81d7744 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ pest_derive = "2.7.8" phf = { version = "0.11.2", features = ["macros"] } const_format = "0.2.32" rpassword = "7.3.1" +hex = "0.4.3" [dev-dependencies] env_logger = "*" diff --git a/Makefile b/Makefile index 726323df..20c89b3a 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,9 @@ install: build test: cargo test +cov: + cargo tarpaulin --bin chsr --bin sr --exclude-files capable* capable-ebpf/src/vmlinux.rs capable/src/main.rs build.rs --out Lcov --out Html + uninstall: rm -f /usr/bin/sr rm -f /usr/bin/chsr diff --git a/book/src/chsr/file-config.md b/book/src/chsr/file-config.md index 76bd3571..71e0aa48 100644 --- a/book/src/chsr/file-config.md +++ b/book/src/chsr/file-config.md @@ -16,9 +16,322 @@ Next, the configuration is divided into roles, tasks, commands, credentials, and ## How configuration work with examples -### Role example +### A complete Config example +The following example shows a RootAsRole config without plugins when almost every field is modified with comments. +```json +{ + "version": "3.0.0-alpha.4", // Version of the configuration file + "storage": { // Storage settings, where the Roles and Execution options are stored + "method": "json", // Storage method + "settings": { // Storage settings + "immutable": false, // Program return error if the file is not immutable, default is true + "path": "target/rootasrole.json" // Path to the storage file + } + }, + "options": { + "path": { // Path options + "default": "delete", // Default policy for path, delete-all, keep-safe, keep-unsafe, inherit + "add": [ // Paths to add to the whitelist + "path1", + "path2" + ], + "sub": [ // Paths to remove from the whitelist + "path3", + "path4" + ] + }, + "env": { // Environment options + "default": "delete", // Default policy for environment, delete-all, keep-all, inherit + "keep": [ // Environment variables to keep + "env1", + "env2" + ], + "check": [ // Environment variables to check for unsafe characters + "env3", + "env4" + ], + "delete": [ // Environment variables to delete + "env5", + "env6" + ] + }, + "root": "privileged", // Default policy for root, privileged, user, inherit + "bounding": "ignore", // Default policy for bounding, strict, ignore, inherit + "wildcard-denied": "*", // Characters denied in any binary path + "timeout": { + "type": "ppid", // Type of timeout, tty, ppid, uid + "duration": "15:30:30", // Duration of the timeout + "max_usage": 1 // Maximum usage before timeout expires + } + }, + "roles": [ // Role list + { + "name": "complete", // Role name + "actors": [ // Actors granted + { + "id": 0, // ID of the actor, could be a name + "type": "user" // Type of actor, user, group + }, + { + "groups": 0, // ID of the group, could be a name + "type": "group" + }, + { + "type": "group", + "groups": [ // List of groups, this is an AND condition between groups + "groupA", + "groupB" + ] + } + ], + "tasks": [ // List of role's tasks + { + "name": "t_complete", // Task name, must be unique in the role + "purpose": "complete", // Task purpose, just a description + "cred": { + "setuid": "user1", // User to setuid before executing the command + "setgid": [ // Groups to setgid before executing the command, The first one is the primary group + "group1", + "group2" + ], + "capabilities": { // Capabilities to grants + "default": "all", // Default policy for capabilities, all, none + "add": [ // Capabilities to add + "CAP_LINUX_IMMUTABLE", + "CAP_NET_BIND_SERVICE" + ], + "sub": [ // Capabilities to remove, overrides add + "CAP_SYS_ADMIN", + "CAP_SYS_BOOT" + ] + } + }, + "commands": { + "default": "all", // Default policy for commands, allow-all, deny-all + "add": [ // Commands to add to the whitelist + "ls", + "echo" + ], + "sub": [ // Commands to add to the blacklist + "cat", + "grep" + ] + }, + "options": { // Task-level options + "path": { + "default": "delete", // When default is not inherit, all upper level options are ignored + "add": [ + "path1", + "path2" + ], + "sub": [ + "path3", + "path4" + ] + }, + "env": { + "default": "delete", + "keep": [ + "env1", + "env2" + ], + "check": [ + "env3", + "env4" + ], + "delete": [ + "env5", + "env6" + ] + }, + "root": "privileged", + "bounding": "ignore", + "wildcard-denied": "*", + "timeout": { + "type": "ppid", + "duration": "15:30:30", + "max_usage": 1 + } + } + } + ], + "options": { // Role-level options + "path": { + "default": "delete", + "add": [ + "path1", + "path2" + ], + "sub": [ + "path3", + "path4" + ] + }, + "env": { + "default": "delete", + "keep": [ + "env1", + "env2" + ], + "check": [ + "env3", + "env4" + ], + "delete": [ + "env5", + "env6" + ] + }, + "root": "privileged", + "bounding": "ignore", + "wildcard-denied": "*", + "timeout": { + "type": "ppid", + "duration": "15:30:30", + "max_usage": 1 + } + } + } + ] +} +``` + +### Config example Role hierarchy plugin + +The following example shows a RootAsRole config using role hierarchy plugin. + +```json +{ + "version": "3.0.0-alpha.4", + "roles": [ + { + "parents": ["user"], + "name": "admin", + "actors": [ + { + "id": 0, + "type": "user" + } + ], + "tasks": [ + ], + }, + { + "name": "user", + "actors": [ + { + "id": 1, + "type": "user" + } + ], + "tasks": [ + { + "name": "t_user", + "purpose": "user", + "commands": { + "default": "all", + "sub": [ + "cat", + "grep" + ] + } + } + ] + } + ] +} +``` + +In this example, the `admin` role inherits from the `user` role. The `user` role has a task `t_user` that denies `cat` and `grep` commands. The `admin` role will inherit the `t_user` task and deny `cat` and `grep` commands. + +### Config example Static separation of duties plugin + +The following example shows a RootAsRole config using separation of duties plugin. + +```json +{ + "version": "3.0.0-alpha.4", + "roles": [ + { + "ssd": ["user"], + "name": "admin", + "actors": [ + { + "id": 0, + "type": "user" + } + ], + "tasks": [ + ], + }, + { + "name": "user", + "actors": [ + { + "id": 0, + "type": "user" + } + ], + "tasks": [ + { + "name": "t_user", + "purpose": "user", + "commands": { + "default": "all", + "sub": [ + "cat", + "grep" + ] + } + } + ] + } + ] +} +``` + +In this example, the `admin` role is separated from the `user` role. The user 0 cannot be in the `user` role and the `admin` role at the same time. But currently this user is still on these two roles. In resulting, the user 0 will not be able to execute any `admin` or `user` role's tasks. + +### Config example with hashchecker plugin + +Hashchecker plugin verifies the integrity of the binary before executing it. The following example shows a RootAsRole config using hashchecker plugin. + +```json +{ + "version": "3.0.0-alpha.4", + "roles": [ + { + "name": "admin", + "actors": [ + { + "id": 0, + "type": "user" + } + ], + "tasks": [ + { + "name": "t_admin", + "purpose": "admin", + "commands": { + "default": "none", + "add": [ + { + "command": "/usr/bin/cat superfile", + "hash_type": "sha256", + "hash": "3b77deacba25588129debfb3b9603d7e7187c29d7f6c14bdb667426b7be91761" + } + ] + } + } + ] + } + ] +} +``` + +This example shows a `t_admin` task that allows the `cat superfile` command only if the hash of the binary is `3b77deacba25588129debfb3b9603d7e7187c29d7f6c14bdb667426b7be91761`. If the hash of the binary is different, the command isn't even considered in configuration setup. Supported hashes : SHA224, SHA256, SHA384, SHA512. ## How options work with examples diff --git a/codecov.yml b/codecov.yml index a61eaf3e..80f71482 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,4 +3,8 @@ ignore: - "tests/**" - "legacy/*" - "legacy/**" - - "src/descriptions.rs" \ No newline at end of file + - "src/descriptions.rs" +coverage: + range: 50..70 + round: down + precision: 2 \ No newline at end of file diff --git a/configure.sh b/configure.sh index 275cfd0b..8ced64e4 100755 --- a/configure.sh +++ b/configure.sh @@ -13,7 +13,7 @@ done if [ -z ${SUDO_USER+x} ]; then INSTALL_USER=`id -urn`; else INSTALL_USER=$SUDO_USER; fi -if [ $(capsh --has-p=CAP_DAC_OVERRIDE; echo $?) != 0 ] || [ $(capsh --has-p=CAP_LINUX_IMMUTABLE; echo $?) != 0 ]; then +if [ $(capsh --has-p=CAP_DAC_OVERRIDE; echo $?) != 0 ] || ( [ ${DOCKER} -eq 0 ] && [ $(capsh --has-p=CAP_LINUX_IMMUTABLE; echo $?) != 0 ] ) ; then echo "Vous avez besoin des capacités CAP_DAC_OVERRIDE et CAP_LINUX_IMMUTABLE pour exécuter ce script." exit 1 fi diff --git a/dependencies.sh b/dependencies.sh index 6d43d6f0..d2eb9f58 100755 --- a/dependencies.sh +++ b/dependencies.sh @@ -11,12 +11,13 @@ while getopts "yd" opt; do esac done -if [ `id -u` -eq 0 ]; then +if [ ! `id -u` -eq 0 ]; then echo "You need to run this script as root" + exit 1 fi echo "Install Rust Cargo compiler" -if [ $(which cargo &>/dev/null ; echo $?) -eq 0 ]; then +if [ $(whereis cargo &>/dev/null ; echo $?) -eq 0 ] && [ -f "/bin/cargo" ]; then echo "Cargo is installed" elif [ "${YES}" == "-y" ]; then curl https://sh.rustup.rs -sSf | sh -s -- -y @@ -24,7 +25,7 @@ else curl https://sh.rustup.rs -sSf | sh fi -if [ ! -f "/usr/bin/cargo" ]; then +if [ ! -f "/bin/cargo" ]; then cp ~/.cargo/bin/cargo /usr/bin ln -s /usr/local/bin/cargo /bin/cargo echo "as $HOME/.cargo/bin/cargo cargo program is copied to /usr/bin" @@ -32,23 +33,25 @@ fi echo "Capabilities & PAM packages installation" if command -v apt-get &>/dev/null; then - apt-get install "${YES}" pkg-config openssl libssl-dev curl gcc llvm clang libcap2 libcap2-bin libcap-dev libcap-ng-dev libelf-dev libpam0g-dev libxml2 libxml2-dev libclang-dev make "linux-headers-$(uname -r)" + apt-get update "${YES}" + apt-get install "${YES}" "linux-headers-$(uname -r)" || apt-get install "${YES}" linux-headers-generic + apt-get install "${YES}" man pkg-config openssl libssl-dev curl gcc llvm clang libcap2 libcap2-bin libcap-dev libcap-ng-dev libelf-dev libpam0g-dev libxml2 libxml2-dev libclang-dev make if [ -n "${DEBUG}" ]; then apt-get install "${YES}" gdb fi; if [ -n "${COV}" ]; then apt-get install "${YES}" gcovr fi; - elif command -v yum &>/dev/null; then - yum install "${YES}" pkgconfig openssl-devel curl gcc llvm clang clang-devel libcap libcap-ng libelf libxml2 libxml2-devel make kernel-headers pam-devel +elif command -v yum &>/dev/null; then + yum install "${YES}" man pkgconfig openssl-devel curl gcc llvm clang clang-devel libcap libcap-ng libelf libxml2 libxml2-devel make kernel-headers pam-devel if [ -n "${DEBUG}" ]; then yum install "${YES}" gdb fi; if [ -n "${COV}" ]; then yum install "${YES}" gcovr fi; - elif command -v pacman &>/dev/null; then - pacman -S "${YES}" pkgconf openssl curl cargo-make gcc llvm clang libcap libcap-ng libelf libxml2 linux-headers linux-api-headers make +elif command -v pacman &>/dev/null; then + pacman -S "${YES}" man pkgconf openssl curl cargo-make gcc llvm clang libcap libcap-ng libelf libxml2 linux-headers linux-api-headers make if [ -n "${DEBUG}" ]; then pacman -S "${YES}" gdb fi; diff --git a/src/api.rs b/src/api.rs index 24178e01..6996412c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -226,7 +226,10 @@ impl PluginManager { for plugin in api.complex_command_parsers.iter() { match plugin(command) { Ok(result) => return Ok(result), - Err(_) => continue, + Err(e) => { + //debug!("Error parsing command {:?}", e); + continue; + } } } Err("No complex command parser found".into()) diff --git a/src/chsr/main.rs b/src/chsr/main.rs index e379b14c..326eb14e 100644 --- a/src/chsr/main.rs +++ b/src/chsr/main.rs @@ -169,7 +169,7 @@ mod tests { fn teardown() { //Remove json test file - std::fs::remove_file("target/rootasrole.json").unwrap(); + std::fs::remove_file(ROOTASROLE).unwrap(); } // we need to test every commands // chsr r r1 create diff --git a/src/config.rs b/src/config.rs index 6ad4f341..d8ca1bb2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -52,7 +52,13 @@ pub const ROOTASROLE: &str = "/etc/security/rootasrole.json"; #[cfg(test)] pub const ROOTASROLE: &str = "target/rootasrole.json"; -use std::{cell::RefCell, error::Error, fs::File, path::PathBuf, rc::Rc}; +use std::{ + cell::RefCell, + error::Error, + fs::File, + path::{Path, PathBuf}, + rc::Rc, +}; use ciborium::de; use serde::{Deserialize, Serialize}; @@ -60,8 +66,8 @@ use tracing::debug; use crate::{ common::{ - dac_override_effective, immutable_effective, read_effective, util::toggle_lock_config, - write_json_config, + dac_override_effective, immutable_effective, open_with_privileges, read_effective, + util::toggle_lock_config, write_json_config, }, rc_refcell, }; @@ -204,8 +210,6 @@ impl Default for RemoteStorageSettings { } pub fn save_settings(settings: Rc>) -> Result<(), Box> { - debug!("Setting immutable privilege"); - immutable_effective(true)?; let default_remote: RemoteStorageSettings = RemoteStorageSettings::default(); // remove immutable flag let into = ROOTASROLE.into(); @@ -218,17 +222,23 @@ pub fn save_settings(settings: Rc>) -> Result<(), Box>> = Versioning::new(settings.clone()); write_json_config(&versionned, ROOTASROLE)?; - debug!("Toggling immutable off for config file"); - toggle_lock_config(path, false)?; + if let Some(settings) = &settings.as_ref().borrow().storage.settings { + if settings.immutable.unwrap_or(true) { + debug!("Toggling immutable off for config file"); + toggle_lock_config(path, false)?; + } + } debug!("Resetting dac privilege"); dac_override_effective(false)?; - debug!("Resetting immutable privilege"); - immutable_effective(false)?; Ok(()) } @@ -238,14 +248,7 @@ pub fn get_settings() -> Result>, Box> { return Ok(rc_refcell!(SettingsFile::default())); } // if user does not have read permission, try to enable privilege - let file = std::fs::File::open(ROOTASROLE).or_else(|e| { - debug!( - "Error opening file without privilege, trying with privileges: {}", - e - ); - read_effective(true).or(dac_override_effective(true))?; - std::fs::File::open(ROOTASROLE) - })?; + let file = open_with_privileges(ROOTASROLE)?; let value: Versioning = serde_json::from_reader(file) .inspect_err(|e| { debug!("Error reading file: {}", e); diff --git a/src/database/finder.rs b/src/database/finder.rs index 1a77406a..b43c6e7c 100644 --- a/src/database/finder.rs +++ b/src/database/finder.rs @@ -270,25 +270,9 @@ pub trait CredMatcher { fn user_matches(&self, user: &Cred) -> UserMin; } -pub fn find_executable_in_path(executable: &str) -> Option { - let path = var("PATH").unwrap_or("".to_string()); - for dir in path.split(':') { - let path = Path::new(dir).join(executable); - if path.exists() { - return Some(path); - } - } - None -} - pub fn parse_conf_command(command: &SCommand) -> Result, Box> { match command { - SCommand::Simple(command) => { - if command == "ALL" { - return Ok(vec!["**".to_string(), ".*".to_string()]); - } - Ok(shell_words::split(command)?) - } + SCommand::Simple(command) => Ok(shell_words::split(command)?), SCommand::Complex(command) => { if let Some(array) = command.as_array() { let mut result = Vec::new(); @@ -306,7 +290,9 @@ pub fn parse_conf_command(command: &SCommand) -> Result, Box Option { None } -fn final_path(path: &String) -> PathBuf { +pub fn final_path(path: &String) -> PathBuf { let result; if let Ok(cannon_path) = std::fs::canonicalize(path) { result = cannon_path; diff --git a/src/database/mod.rs b/src/database/mod.rs index e2cb02e4..fcdddc57 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -14,12 +14,12 @@ use self::{migration::Migration, options::EnvKey, structs::SConfig, version::Ver use super::config::SettingsFile; use super::util::warn_if_mutable; -use super::write_json_config; use super::{ config::{RemoteStorageSettings, ROOTASROLE}, dac_override_effective, immutable_effective, util::parse_capset_iter, }; +use super::{open_with_privileges, write_json_config}; pub mod finder; pub mod migration; @@ -55,7 +55,7 @@ pub fn read_json_config( make_weak_config(&settings.as_ref().borrow().config); Ok(settings.as_ref().borrow().config.clone()) } else { - let file = std::fs::File::open(path)?; + let file = open_with_privileges(path)?; warn_if_mutable( &file, settings @@ -87,7 +87,6 @@ pub fn save_json( config: Rc>, ) -> Result<(), Box> { let default_remote: RemoteStorageSettings = RemoteStorageSettings::default(); - // remove immutable flag let into = ROOTASROLE.into(); let binding = settings.as_ref().borrow(); let path = binding @@ -102,21 +101,25 @@ pub fn save_json( // if /etc/security/rootasrole.json then you need to consider the settings to save in addition to the config return save_settings(settings.clone()); } - debug!("Setting immutable privilege"); - immutable_effective(true)?; - debug!("Toggling immutable on for config file"); - toggle_lock_config(path, true)?; - immutable_effective(false)?; + debug!("Writing config file"); let versionned: Versioning>> = Versioning { version: PACKAGE_VERSION.to_owned().parse()?, data: config, }; + if let Some(settings) = &settings.as_ref().borrow().storage.settings { + if settings.immutable.unwrap_or(true) { + debug!("Toggling immutable on for config file"); + toggle_lock_config(path, true)?; + } + } write_sconfig(&settings.as_ref().borrow(), versionned)?; - debug!("Toggling immutable off for config file"); - immutable_effective(true)?; - toggle_lock_config(path, false)?; - + if let Some(settings) = &settings.as_ref().borrow().storage.settings { + if settings.immutable.unwrap_or(true) { + debug!("Toggling immutable off for config file"); + toggle_lock_config(path, false)?; + } + } debug!("Resetting immutable privilege"); immutable_effective(false)?; Ok(()) diff --git a/src/descriptions.rs b/src/descriptions.rs index b10248e6..df4e4866 100644 --- a/src/descriptions.rs +++ b/src/descriptions.rs @@ -6,47 +6,6 @@ use capctl::Cap; #[allow(clippy::all)] pub fn get_capability_description(cap : &Cap) -> &'static str { match *cap { - Cap::AUDIT_CONTROL => r#"(since Linux 2.6.11) Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules."#, - Cap::AUDIT_READ => r#"(since Linux 3.16) Allow reading the audit log via a multicast netlink socket."#, - Cap::AUDIT_WRITE => r#"(since Linux 2.6.11) Write records to kernel auditing log."#, - Cap::BLOCK_SUSPEND => r#"(since Linux 3.5) Employ features that can block system suspend (epoll(7) EPOLLWAKEUP, /proc/sys/wake_lock)."#, - Cap::BPF => r#"(since Linux 5.8) Employ privileged BPF operations; see bpf(2) and bpf-helpers(7). This capability was added in Linux 5.8 to separate out BPF functionality from the overloaded CAP_SYS_ADMIN capability."#, - Cap::CHECKPOINT_RESTORE => r#"(since Linux 5.9) • Update /proc/sys/kernel/ns_last_pid (see pid_namespaces(7)); • employ the set_tid feature of clone3(2); • read the contents of the symbolic links in /proc/pid/map_files for other processes. This capability was added in Linux 5.9 to separate out checkpoint/restore functionality from the overloaded CAP_SYS_ADMIN capability."#, - Cap::CHOWN => r#"Make arbitrary changes to file UIDs and GIDs (see chown(2))."#, - Cap::DAC_OVERRIDE => r#"Bypass file read, write, and execute permission checks. (DAC is an abbreviation of \"discretionary access control\".)"#, - Cap::DAC_READ_SEARCH => r#"• Bypass file read permission checks and directory read and execute permission checks; • invoke open_by_handle_at(2); • use the linkat(2) AT_EMPTY_PATH flag to create a link to a file referred to by a file descriptor."#, - Cap::FOWNER => r#"• Bypass permission checks on operations that normally require the filesystem UID of the process to match the UID of the file (e.g., chmod(2), utime(2)), excluding those operations covered by CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH; • set inode flags (see ioctl_iflags(2)) on arbitrary files; • set Access Control Lists (ACLs) on arbitrary files; • ignore directory sticky bit on file deletion; • modify user extended attributes on sticky directory owned by any user; • specify O_NOATIME for arbitrary files in open(2) and fcntl(2)."#, - Cap::FSETID => r#"• Don't clear set-user-ID and set-group-ID mode bits when a file is modified; • set the set-group-ID bit for a file whose GID does not match the filesystem or any of the supplementary GIDs of the calling process."#, - Cap::IPC_LOCK => r#"• Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2)); • Allocate memory using huge pages (memfd_create(2), mmap(2), shmctl(2))."#, - Cap::IPC_OWNER => r#"Bypass permission checks for operations on System V IPC objects."#, - Cap::KILL => r#"Bypass permission checks for sending signals (see kill(2)). This includes use of the ioctl(2) KDSIGACCEPT operation."#, - Cap::LEASE => r#"(since Linux 2.4) Establish leases on arbitrary files (see fcntl(2))."#, - Cap::LINUX_IMMUTABLE => r#"Set the FS_APPEND_FL and FS_IMMUTABLE_FL inode flags (see ioctl_iflags(2))."#, - Cap::MAC_ADMIN => r#"(since Linux 2.6.25) Allow MAC configuration or state changes. Implemented for the Smack Linux Security Module (LSM)."#, - Cap::MAC_OVERRIDE => r#"(since Linux 2.6.25) Override Mandatory Access Control (MAC). Implemented for the Smack LSM."#, - Cap::MKNOD => r#"(since Linux 2.4) Create special files using mknod(2)."#, - Cap::NET_ADMIN => r#"Perform various network-related operations: • interface configuration; • administration of IP firewall, masquerading, and accounting; • modify routing tables; • bind to any address for transparent proxying; • set type-of-service (TOS); • clear driver statistics; • set promiscuous mode; • enabling multicasting; • use setsockopt(2) to set the following socket options: SO_DEBUG, SO_MARK, SO_PRIORITY (for a priority outside the range 0 to 6), SO_RCVBUFFORCE, and SO_SNDBUFFORCE."#, - Cap::NET_BIND_SERVICE => r#"Bind a socket to Internet domain privileged ports (port numbers less than 1024)."#, - Cap::NET_BROADCAST => r#"(Unused) Make socket broadcasts, and listen to multicasts."#, - Cap::NET_RAW => r#"• Use RAW and PACKET sockets; • bind to any address for transparent proxying."#, - Cap::PERFMON => r#"(since Linux 5.8) Employ various performance-monitoring mechanisms, including: • call perf_event_open(2); • employ various BPF operations that have performance implications. This capability was added in Linux 5.8 to separate out performance monitoring functionality from the overloaded CAP_SYS_ADMIN capability. See also the kernel source file Documentation/admin-guide/perf-security.rst."#, - Cap::SETGID => r#"• Make arbitrary manipulations of process GIDs and supplementary GID list; • forge GID when passing socket credentials via UNIX domain sockets; • write a group ID mapping in a user namespace (see user_namespaces(7))."#, - Cap::SETFCAP => r#"(since Linux 2.6.24) Set arbitrary capabilities on a file. Since Linux 5.12, this capability is also needed to map user ID 0 in a new user namespace; see user_namespaces(7) for details."#, - Cap::SETPCAP => r#"If file capabilities are supported (i.e., since Linux 2.6.24): add any capability from the calling thread's bounding set to its inheritable set; drop capabilities from the bounding set (via prctl(2) PR_CAPBSET_DROP); make changes to the securebits flags. If file capabilities are not supported (i.e., before Linux 2.6.24): grant or remove any capability in the caller's permitted capability set to or from any other process. (This property of CAP_SETPCAP is not available when the kernel is configured to support file capabilities, since CAP_SETPCAP has entirely different semantics for such kernels.)"#, - Cap::SETUID => r#"• Make arbitrary manipulations of process UIDs (setuid(2), setreuid(2), setresuid(2), setfsuid(2)); • forge UID when passing socket credentials via UNIX domain sockets; • write a user ID mapping in a user namespace (see user_namespaces(7))."#, - Cap::SYS_ADMIN => r#"Note: this capability is overloaded; see Notes to kernel developers below. • Perform a range of system administration operations including: quotactl(2), mount(2), umount(2), pivot_root(2), swapon(2), swapoff(2), sethostname(2), and setdomainname(2); • perform privileged syslog(2) operations (since Linux 2.6.37, CAP_SYSLOG should be used to permit such operations); • perform VM86_REQUEST_IRQ vm86(2) command; • access the same checkpoint/restore functionality that is governed by CAP_CHECKPOINT_RESTORE (but the latter, weaker capability is preferred for accessing that functionality). • perform the same BPF operations as are governed by CAP_BPF (but the latter, weaker capability is preferred for accessing that functionality). • employ the same performance monitoring mechanisms as are governed by CAP_PERFMON (but the latter, weaker capability is preferred for accessing that functionality). • perform IPC_SET and IPC_RMID operations on arbitrary System V IPC objects; • override RLIMIT_NPROC resource limit; • perform operations on trusted and security extended attributes (see xattr(7)); • use lookup_dcookie(2); • use ioprio_set(2) to assign IOPRIO_CLASS_RT and (before Linux 2.6.25) IOPRIO_CLASS_IDLE I/O scheduling classes; • forge PID when passing socket credentials via UNIX domain sockets; • exceed /proc/sys/fs/file-max, the system-wide limit on the number of open files, in system calls that open files (e.g., accept(2), execve(2), open(2), pipe(2)); • employ CLONE_* flags that create new namespaces with clone(2) and unshare(2) (but, since Linux 3.8, creating user namespaces does not require any capability); • access privileged perf event information; • call setns(2) (requires CAP_SYS_ADMIN in the target namespace); • call fanotify_init(2); • perform privileged KEYCTL_CHOWN and KEYCTL_SETPERM keyctl(2) operations; • perform madvise(2) MADV_HWPOISON operation; • employ the TIOCSTI ioctl(2) to insert characters into the input queue of a terminal other than the caller's controlling terminal; • employ the obsolete nfsservctl(2) system call; • employ the obsolete bdflush(2) system call; • perform various privileged block-device ioctl(2) operations; • perform various privileged filesystem ioctl(2) operations; • perform privileged ioctl(2) operations on the /dev/random device (see random(4)); • install a seccomp(2) filter without first having to set the no_new_privs thread attribute; • modify allow/deny rules for device control groups; • employ the ptrace(2) PTRACE_SECCOMP_GET_FILTER operation to dump tracee's seccomp filters; • employ the ptrace(2) PTRACE_SETOPTIONS operation to suspend the tracee's seccomp protections (i.e., the PTRACE_O_SUSPEND_SECCOMP flag); • perform administrative operations on many device drivers; • modify autogroup nice values by writing to /proc/pid/autogroup (see sched(7))."#, - Cap::SYS_BOOT => r#"Use reboot(2) and kexec_load(2)."#, - Cap::SYS_CHROOT => r#"• Use chroot(2); • change mount namespaces using setns(2)."#, - Cap::SYS_MODULE => r#"• Load and unload kernel modules (see init_module(2) and delete_module(2)); • before Linux 2.6.25: drop capabilities from the system-wide capability bounding set."#, - Cap::SYS_NICE => r#"• Lower the process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes; • set real-time scheduling policies for calling process, and set scheduling policies and priorities for arbitrary processes (sched_setscheduler(2), sched_setparam(2), sched_setattr(2)); • set CPU affinity for arbitrary processes (sched_setaffinity(2)); • set I/O scheduling class and priority for arbitrary processes (ioprio_set(2)); • apply migrate_pages(2) to arbitrary processes and allow processes to be migrated to arbitrary nodes; • apply move_pages(2) to arbitrary processes; • use the MPOL_MF_MOVE_ALL flag with mbind(2) and move_pages(2)."#, - Cap::SYS_PACCT => r#"Use acct(2)."#, - Cap::SYS_PTRACE => r#"• Trace arbitrary processes using ptrace(2); • apply get_robust_list(2) to arbitrary processes; • transfer data to or from the memory of arbitrary processes using process_vm_readv(2) and process_vm_writev(2); • inspect processes using kcmp(2)."#, - Cap::SYS_RAWIO => r#"• Perform I/O port operations (iopl(2) and ioperm(2)); • access /proc/kcore; • employ the FIBMAP ioctl(2) operation; • open devices for accessing x86 model-specific registers (MSRs, see msr(4)); • update /proc/sys/vm/mmap_min_addr; • create memory mappings at addresses below the value specified by /proc/sys/vm/mmap_min_addr; • map files in /proc/bus/pci; • open /dev/mem and /dev/kmem; • perform various SCSI device commands; • perform certain operations on hpsa(4) and cciss(4) devices; • perform a range of device-specific operations on other devices."#, - Cap::SYS_RESOURCE => r#"• Use reserved space on ext2 filesystems; • make ioctl(2) calls controlling ext3 journaling; • override disk quota limits; • increase resource limits (see setrlimit(2)); • override RLIMIT_NPROC resource limit; • override maximum number of consoles on console allocation; • override maximum number of keymaps; • allow more than 64hz interrupts from the real-time clock; • raise msg_qbytes limit for a System V message queue above the limit in /proc/sys/kernel/msgmnb (see msgop(2) and msgctl(2)); • allow the RLIMIT_NOFILE resource limit on the number of \"in-flight\" file descriptors to be bypassed when passing file descriptors to another process via a UNIX domain socket (see unix(7)); • override the /proc/sys/fs/pipe-size-max limit when setting the capacity of a pipe using the F_SETPIPE_SZ fcntl(2) command; • use F_SETPIPE_SZ to increase the capacity of a pipe above the limit specified by /proc/sys/fs/pipe-max-size; • override /proc/sys/fs/mqueue/queues_max, /proc/sys/fs/mqueue/msg_max, and /proc/sys/fs/mqueue/msgsize_max limits when creating POSIX message queues (see mq_overview(7)); • employ the prctl(2) PR_SET_MM operation; • set /proc/pid/oom_score_adj to a value lower than the value last set by a process with CAP_SYS_RESOURCE."#, - Cap::SYS_TIME => r#"Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock."#, - Cap::SYS_TTY_CONFIG => r#"Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals."#, - Cap::SYSLOG => r#"(since Linux 2.6.37) • Perform privileged syslog(2) operations. See syslog(2) for information on which operations require privilege. • View kernel addresses exposed via /proc and other interfaces when /proc/sys/kernel/kptr_restrict has the value 1. (See the discussion of the kptr_restrict in proc(5).)"#, - Cap::WAKE_ALARM => r#"(since Linux 3.0) Trigger something that will wake up the system (set CLOCK_REALTIME_ALARM and CLOCK_BOOTTIME_ALARM timers)."#, _ => "Unknown capability", } } \ No newline at end of file diff --git a/src/mod.rs b/src/mod.rs index c197a0f8..99959170 100644 --- a/src/mod.rs +++ b/src/mod.rs @@ -1,6 +1,11 @@ use capctl::{prctl, Cap, CapState}; use serde::Serialize; -use std::{error::Error, ffi::CString, path::PathBuf}; +use std::{ + error::Error, + ffi::CString, + fs::File, + path::{Path, PathBuf}, +}; use tracing::{debug, Level}; use tracing_subscriber::util::SubscriberInitExt; @@ -96,18 +101,38 @@ pub fn activates_no_new_privs() -> Result<(), capctl::Error> { prctl::set_no_new_privs() } -pub fn write_json_config>( - settings: &T, - path: S, -) -> Result<(), Box> { - let file = std::fs::File::create(path).or_else(|e| { +pub fn write_json_config(settings: &T, path: S) -> Result<(), Box> +where + S: std::convert::AsRef + Clone, +{ + let file = create_with_privileges(path)?; + serde_json::to_writer_pretty(file, &settings)?; + Ok(()) +} + +pub fn create_with_privileges>(p: P) -> Result { + std::fs::File::create(&p).or_else(|e| { + debug!( + "Error creating file without privilege, trying with privileges: {}", + e + ); + dac_override_effective(true)?; + let res = std::fs::File::create(p); + dac_override_effective(false)?; + res + }) +} + +pub fn open_with_privileges>(p: P) -> Result { + std::fs::File::open(&p).or_else(|e| { debug!( "Error creating file without privilege, trying with privileges: {}", e ); read_effective(true).or(dac_override_effective(true))?; - std::fs::File::create(ROOTASROLE) - })?; - serde_json::to_writer_pretty(file, &settings)?; - Ok(()) + let res = std::fs::File::open(p); + read_effective(false)?; + dac_override_effective(false)?; + res + }) } diff --git a/src/plugin/hashchecker.rs b/src/plugin/hashchecker.rs index f0111d05..12f0c13e 100644 --- a/src/plugin/hashchecker.rs +++ b/src/plugin/hashchecker.rs @@ -1,18 +1,20 @@ use serde::{Deserialize, Serialize}; -use sha1::Digest; +use tracing::debug; +use tracing_subscriber::field::debug; use crate::common::{ api::PluginManager, - database::finder::{find_executable_in_path, parse_conf_command}, - database::structs::SCommand, + database::{ + finder::{final_path, parse_conf_command}, + structs::SCommand, + }, }; -use md5; +use sha2::Digest; #[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum HashType { - MD5, - SHA1, SHA224, SHA256, SHA384, @@ -26,14 +28,8 @@ struct HashChecker { command: SCommand, } -fn compute(hashtype: &HashType, hash: &str) -> Vec { +fn compute(hashtype: &HashType, hash: &[u8]) -> Vec { match hashtype { - HashType::MD5 => md5::compute(hash).0.to_vec(), - HashType::SHA1 => { - let mut hasher = sha1::Sha1::new(); - hasher.update(hash); - hasher.finalize().to_vec() - } HashType::SHA224 => { let mut hasher = sha2::Sha224::new(); hasher.update(hash); @@ -61,33 +57,95 @@ fn complex_command_parse( command: &serde_json::Value, ) -> Result, Box> { let checker = serde_json::from_value::(command.clone()); - + debug!("Checking command {:?}", checker); match checker { Ok(checker) => { - let path; - if let SCommand::Simple(command) = &checker.command { - let opath = find_executable_in_path(command); - if opath.is_none() { - return Err("Command not found".into()); - } - path = opath.unwrap(); - } else { - return Err("Invalid command".into()); - } - if compute( - &checker.hash_type, - &String::from_utf8(std::fs::read(path)?)?, - ) == compute(&checker.hash_type, &checker.hash) - { + let cmd = parse_conf_command(&checker.command)?; + let path = final_path(&cmd[0]); + let hash = compute(&checker.hash_type, &std::fs::read(path)?); + let config_hash = hex::decode(checker.hash.as_bytes())?; + debug!( + "Hash: {:?}, Config Hash: {:?}", + hex::encode(&hash), + hex::encode(&config_hash) + ); + if hash == config_hash { + debug!("Hashes match"); parse_conf_command(&checker.command) } else { + debug!("Hashes do not match"); Err("Hashes do not match".into()) } } - Err(e) => Err(Box::new(e)), + Err(e) => { + debug!("Error parsing command {:?}", e); + Err(Box::new(e)) + } } } pub fn register() { PluginManager::subscribe_complex_command_parser(complex_command_parse) } + +#[cfg(test)] +mod tests { + + use std::{io::Write, rc::Rc}; + + use nix::unistd::{Pid, User}; + + use super::*; + use crate::{ + common::database::{ + finder::{Cred, TaskMatcher}, + structs::{IdTask, SActor, SCommand, SCommands, SConfig, SRole, STask}, + }, + rc_refcell, + }; + + #[test] + fn test_plugin_implemented() { + register(); + // create a file in /tmp + let mut file = std::fs::File::create("/tmp/hashchecker").unwrap(); + file.write("test".as_bytes()).unwrap(); + file.sync_all().unwrap(); + + let config = rc_refcell!(SConfig::default()); + let role1 = rc_refcell!(SRole::default()); + role1.as_ref().borrow_mut()._config = Some(Rc::downgrade(&config)); + role1.as_ref().borrow_mut().name = "role1".to_string(); + let task1 = rc_refcell!(STask::default()); + task1.as_ref().borrow_mut()._role = Some(Rc::downgrade(&role1)); + task1.as_ref().borrow_mut().name = IdTask::Name("task1".to_string()); + let mut command = SCommands::default(); + command.add.push(SCommand::Complex(serde_json::json!({ + "hash_type": "sha256", + "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + "command": "/tmp/hashchecker" + }))); + task1.as_ref().borrow_mut().commands = command; + role1.as_ref().borrow_mut().tasks.push(task1); + role1 + .as_ref() + .borrow_mut() + .actors + .push(SActor::from_user_id(0)); + + config.as_ref().borrow_mut().roles.push(role1); + + let cred = Cred { + user: User::from_uid(0.into()).unwrap().unwrap(), + groups: vec![], + ppid: Pid::parent(), + tty: None, + }; + + let matching = config + .matches(&cred, &vec!["/tmp/hashchecker".to_string()]) + .unwrap(); + assert!(matching.fully_matching()); + std::fs::remove_file("/tmp/hashchecker").unwrap(); + } +} diff --git a/src/sr/main.rs b/src/sr/main.rs index 3e238a55..c7973443 100644 --- a/src/sr/main.rs +++ b/src/sr/main.rs @@ -4,7 +4,8 @@ mod timeout; use capctl::CapState; use clap::Parser; -use common::database::finder::{Cred, TaskMatch, TaskMatcher}; +use common::database::finder::{Cred, CredMatcher, TaskMatch, TaskMatcher}; +use common::database::structs::IdTask; use common::database::{options::OptStack, structs::SConfig}; use nix::{ libc::dev_t, @@ -31,6 +32,13 @@ use crate::common::{ }; use crate::common::{drop_effective, subsribe}; +#[cfg(not(test))] +const PAM_SERVICE: &str = "sr"; +#[cfg(test)] +const PAM_SERVICE: &str = "sr_test"; + +const PAM_PROMPT: &str = "Password: "; + #[derive(Parser, Debug)] #[command( about = "Execute privileged commands with a role-based access control system", @@ -50,7 +58,7 @@ struct Cli { task: Option, /// Prompt option allows you to override the default password prompt and use a custom one. - #[arg(short, long, default_value = "Password: ")] + #[arg(short, long, default_value = PAM_PROMPT)] prompt: String, /// Display rights of executor @@ -173,14 +181,27 @@ fn from_json_execution_settings( .expect("Permission Denied") .matches(user, &args.command) .map_err(|m| m.into()), - (Some(role), Some(task)) => as_borrow!(config) - .task( - role, - &common::database::structs::IdTask::Name(task.to_string()), - ) - .expect("Permission Denied") - .matches(user, &args.command) - .map_err(|m| m.into()), + (Some(role), Some(task)) => { + let task = IdTask::Name(task.to_string()); + let res = as_borrow!(config) + .role(role) + .expect("Permission Denied") + .matches(user, &args.command)?; + if res.fully_matching() && res.settings.task().as_ref().borrow().name == task { + Ok(res) + } else { + let mut taskres = as_borrow!(config) + .task(role, &task) + .expect("Permission Denied") + .matches(user, &args.command)?; + if taskres.command_matching() { + taskres.score.user_min = res.score.user_min; + Ok(taskres) + } else { + Err("Permission Denied".into()) + } + } + } (None, Some(_)) => Err("You must specify a role to designate a task".into()), } } @@ -230,9 +251,6 @@ fn main() -> Result<(), Box> { tty, ppid, }; - - dac_override_effective(true) - .unwrap_or_else(|_| panic!("{}", cap_effective_error("dac_override"))); let config = match settings.clone().as_ref().borrow().storage.method { config::StorageMethod::JSON => { Storage::JSON(read_json_config(settings).expect("Failed to read config")) @@ -283,6 +301,68 @@ fn main() -> Result<(), Box> { debug!("setuid : {:?}", execcfg.setuid); + setuid_setgid(execcfg); + + set_capabilities(execcfg, optstack); + + //execute command + let envset = optstack + .calculate_filtered_env() + .expect("Failed to calculate env"); + let pty = Pty::new().expect("Failed to create pty"); + + let command = Command::new(&execcfg.exec_path) + .args(execcfg.exec_args.iter()) + .envs(envset) + .stdin(std::process::Stdio::inherit()) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .spawn(&pty.pts().expect("Failed to get pts")); + let mut command = match command { + Ok(command) => command, + Err(e) => { + error!("{}", e); + /*error!( + "{} : command not found", + matching.exec_path.display() + );*/ + eprintln!("sr: {} : command not found", execcfg.exec_path.display()); + std::process::exit(1); + } + }; + let status = command.wait().expect("Failed to wait for command"); + std::process::exit(status.code().unwrap_or(1)); +} + +fn set_capabilities(execcfg: &common::database::finder::ExecSettings, optstack: &OptStack) { + //set capabilities + if let Some(caps) = execcfg.caps { + setpcap_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); + let mut capstate = CapState::empty(); + if !optstack.get_bounding().1.is_ignore() { + for cap in (!caps).iter() { + capctl::bounding::drop(cap).expect("Failed to set bounding cap"); + } + } + capstate.permitted = caps; + capstate.inheritable = caps; + capstate.set_current().expect("Failed to set current cap"); + for cap in caps.iter() { + capctl::ambient::raise(cap).expect("Failed to set ambiant cap"); + } + setpcap_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); + } else { + setpcap_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); + if !optstack.get_bounding().1.is_ignore() { + capctl::bounding::clear().expect("Failed to clear bounding cap"); + } + let capstate = CapState::empty(); + capstate.set_current().expect("Failed to set current cap"); + setpcap_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); + } +} + +fn setuid_setgid(execcfg: &common::database::finder::ExecSettings) { let uid = execcfg.setuid.as_ref().and_then(|u| { let res = u.into_user().unwrap_or(None); if let Some(user) = res { @@ -335,60 +415,6 @@ fn main() -> Result<(), Box> { capctl::cap_set_ids(uid, gid, groups.as_deref()).expect("Failed to set ids"); setgid_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setgid"))); setuid_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setuid"))); - - //set capabilities - if let Some(caps) = execcfg.caps { - setpcap_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); - let mut capstate = CapState::empty(); - if !optstack.get_bounding().1.is_ignore() { - for cap in (!caps).iter() { - capctl::bounding::drop(cap).expect("Failed to set bounding cap"); - } - } - capstate.permitted = caps; - capstate.inheritable = caps; - capstate.set_current().expect("Failed to set current cap"); - for cap in caps.iter() { - capctl::ambient::raise(cap).expect("Failed to set ambiant cap"); - } - setpcap_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); - } else { - setpcap_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); - if !optstack.get_bounding().1.is_ignore() { - capctl::bounding::clear().expect("Failed to clear bounding cap"); - } - let capstate = CapState::empty(); - capstate.set_current().expect("Failed to set current cap"); - setpcap_effective(false).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); - } - - //execute command - let envset = optstack - .calculate_filtered_env() - .expect("Failed to calculate env"); - let pty = Pty::new().expect("Failed to create pty"); - - let command = Command::new(&execcfg.exec_path) - .args(execcfg.exec_args.iter()) - .envs(envset) - .stdin(std::process::Stdio::inherit()) - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::inherit()) - .spawn(&pty.pts().expect("Failed to get pts")); - let mut command = match command { - Ok(command) => command, - Err(e) => { - error!("{}", e); - /*error!( - "{} : command not found", - matching.exec_path.display() - );*/ - eprintln!("sr: {} : command not found", execcfg.exec_path.display()); - std::process::exit(1); - } - }; - let status = command.wait().expect("Failed to wait for command"); - std::process::exit(status.code().unwrap_or(1)); } fn check_auth( @@ -404,7 +430,7 @@ fn check_auth( debug!("need to re-authenticate : {}", !is_valid); if !is_valid { let mut context = Context::new( - "sr", + PAM_SERVICE, Some(&user.user.name), SrConversationHandler::new(prompt), ) @@ -419,3 +445,81 @@ fn check_auth( } Ok(()) } + +#[cfg(test)] +mod tests { + use nix::unistd::Pid; + + use super::*; + use crate::common::database::finder::TaskMatch; + use crate::common::database::make_weak_config; + use crate::common::database::structs::{ + IdTask, SActor, SCommand, SCommands, SConfig, SRole, STask, + }; + use std::cell::RefCell; + use std::rc::Rc; + + #[test] + fn test_from_json_execution_settings() { + let mut args = Cli { + role: None, + task: None, + prompt: PAM_PROMPT.to_string(), + info: false, + command: vec!["ls".to_string(), "-l".to_string()], + }; + let user = Cred { + user: User::from_uid(0.into()).unwrap().unwrap(), + groups: vec![], + tty: None, + ppid: Pid::parent(), + }; + let config = rc_refcell!(SConfig::default()); + let role = rc_refcell!(SRole::default()); + let task = rc_refcell!(STask::default()); + task.as_ref().borrow_mut().name = IdTask::Name("task1".to_owned()); + task.as_ref().borrow_mut().commands = SCommands::default(); + task.as_ref() + .borrow_mut() + .commands + .add + .push(SCommand::Simple("ls -l".to_owned())); + role.as_ref().borrow_mut().name = "role1".to_owned(); + role.as_ref() + .borrow_mut() + .actors + .push(SActor::from_user_id(0)); + role.as_ref().borrow_mut().tasks.push(task); + let task = rc_refcell!(STask::default()); + task.as_ref().borrow_mut().name = IdTask::Name("task2".to_owned()); + task.as_ref().borrow_mut().commands = SCommands::default(); + task.as_ref() + .borrow_mut() + .commands + .add + .push(SCommand::Simple("ls .*".to_owned())); + role.as_ref().borrow_mut().tasks.push(task); + let task = rc_refcell!(STask::default()); + task.as_ref().borrow_mut().name = IdTask::Name("task3".to_owned()); + role.as_ref().borrow_mut().tasks.push(task); + config.as_ref().borrow_mut().roles.push(role); + make_weak_config(&config); + let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); + assert!(taskmatch.fully_matching()); + args.role = Some("role1".to_owned()); + let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); + assert!(taskmatch.fully_matching()); + args.task = Some("task1".to_owned()); + let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); + assert!(taskmatch.fully_matching()); + args.task = Some("task2".to_owned()); + let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); + assert!(taskmatch.fully_matching()); + args.task = Some("task3".to_owned()); + let taskmatch = from_json_execution_settings(&args, &config, &user); + assert!(taskmatch.is_err()); + args.role = None; + let taskmatch = from_json_execution_settings(&args, &config, &user); + assert!(taskmatch.is_err()); + } +} diff --git a/src/sr/timeout.rs b/src/sr/timeout.rs index e1049cc5..c36bd407 100644 --- a/src/sr/timeout.rs +++ b/src/sr/timeout.rs @@ -1,6 +1,6 @@ use std::{ error::Error, - fs::{self, File}, + fs, io::{BufReader, Read, Write}, path::Path, thread::sleep, @@ -16,9 +16,13 @@ use nix::{ use serde::{Deserialize, Serialize}; use tracing::debug; -use crate::common::database::{ - finder::Cred, - options::{STimeout, TimestampType}, +use crate::common::{ + create_with_privileges, dac_override_effective, + database::{ + finder::Cred, + options::{STimeout, TimestampType}, + }, + open_with_privileges, }; /// This module checks the validity of a user's credentials @@ -90,7 +94,7 @@ fn wait_for_lockfile(lockfile_path: &Path) -> Result<(), Box> { let retry_interval = time::Duration::from_secs(1); let pid_contents: pid_t; if lockfile_path.exists() { - if let Ok(mut lockfile) = File::open(lockfile_path) { + if let Ok(mut lockfile) = open_with_privileges(lockfile_path) { let mut be: [u8; 4] = [u8::MAX; 4]; if lockfile.read_exact(&mut be).is_err() { debug!( @@ -146,7 +150,7 @@ fn wait_for_lockfile(lockfile_path: &Path) -> Result<(), Box> { } fn write_lockfile(lockfile_path: &Path) { - let mut lockfile = File::create(lockfile_path).expect("Failed to create lockfile"); + let mut lockfile = create_with_privileges(lockfile_path).expect("Failed to create lockfile"); let pid_contents = nix::unistd::getpid().as_raw(); lockfile .write_all(&pid_contents.to_be_bytes()) @@ -165,7 +169,7 @@ fn read_cookies(user: &Cred) -> Result, Box> { } wait_for_lockfile(&lockpath)?; write_lockfile(&lockpath); - let mut file = File::open(&path)?; + let mut file = open_with_privileges(&path)?; let reader = BufReader::new(&mut file); let res = ciborium::de::from_reader::, BufReader<_>>(reader)?; Ok(res) @@ -173,11 +177,20 @@ fn read_cookies(user: &Cred) -> Result, Box> { fn save_cookies(user: &Cred, cookies: &[CookieVersion]) -> Result<(), Box> { let path = Path::new(TS_LOCATION).join(&user.user.name); - fs::create_dir_all(path.parent().unwrap())?; + fs::create_dir_all(path.parent().unwrap()).or_else(|e| { + debug!( + "Failed to create directory for cookies: {}, trying with privileges", + e + ); + dac_override_effective(true)?; + let res = fs::create_dir_all(path.parent().unwrap()); + dac_override_effective(false)?; + res + })?; let lockpath = Path::new(TS_LOCATION) .join(&user.user.name) .with_extension("lock"); - let mut file = File::create(&path)?; + let mut file = create_with_privileges(&path)?; ciborium::ser::into_writer(cookies, &mut file)?; if let Err(err) = fs::remove_file(lockpath) { debug!("Failed to remove lockfile: {}", err); @@ -226,8 +239,8 @@ fn find_valid_cookie( for a in to_remove { cookies.remove(a); } - if save_cookies(from, &cookies).is_err() { - debug!("Failed to save cookies"); + if let Err(e) = save_cookies(from, &cookies) { + debug!("Failed to save cookies {:?}", e); } res } diff --git a/src/util.rs b/src/util.rs index bbcc08bc..15613ecc 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,7 +2,9 @@ use std::{error::Error, fs::File, os::fd::AsRawFd, path::PathBuf}; use capctl::{Cap, CapSet, ParseCapError}; use libc::{FS_IOC_GETFLAGS, FS_IOC_SETFLAGS}; -use tracing::warn; +use tracing::{debug, warn}; + +use crate::common::{immutable_effective, open_with_privileges}; #[macro_export] macro_rules! upweak { @@ -35,7 +37,7 @@ macro_rules! rc_refcell { const FS_IMMUTABLE_FL: u32 = 0x00000010; pub fn toggle_lock_config(file: &PathBuf, lock: bool) -> Result<(), String> { - let file = match File::open(file) { + let file = match open_with_privileges(file) { Err(e) => return Err(e.to_string()), Ok(f) => f, }; @@ -49,9 +51,13 @@ pub fn toggle_lock_config(file: &PathBuf, lock: bool) -> Result<(), String> { } else { val |= FS_IMMUTABLE_FL; } + debug!("Setting immutable privilege"); + immutable_effective(true).map_err(|e| e.to_string())?; if unsafe { nix::libc::ioctl(fd, FS_IOC_SETFLAGS, &mut val) } < 0 { return Err(std::io::Error::last_os_error().to_string()); } + debug!("Resetting immutable privilege"); + immutable_effective(false).map_err(|e| e.to_string())?; Ok(()) }