diff --git a/.cargo/config.toml b/.cargo/config.toml index 292e7054..0fa4b2e1 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,25 @@ [alias] xtask = "run --package xtask --release --bin xtask --" + +[env] +RAR_CFG_TYPE = "json" +RAR_CFG_PATH = "/etc/security/rootasrole.json" +RAR_CFG_DATA_PATH = "/etc/security/rootasrole.json" +RAR_BIN_PATH = "/usr/bin" +RAR_CFG_IMMUTABLE = "true" +RAR_TIMEOUT_TYPE = "ppid" +RAR_TIMEOUT_DURATION = "00:05:00" +RAR_TIMEOUT_MAX_USAGE = "" +RAR_PATH_DEFAULT = "delete" +RAR_PATH_ADD_LIST = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" +RAR_PATH_REMOVE_LIST = "" +RAR_ENV_DEFAULT = "delete" +RAR_ENV_KEEP_LIST = "HOME,USER,LOGNAME,COLORS,DISPLAY,HOSTNAME,KRB5CCNAME,LS_COLORS,PS1,PS2,XAUTHORY,XAUTHORIZATION,XDG_CURRENT_DESKTOP" +RAR_ENV_CHECK_LIST = "COLORTERM,LANG,LANGUAGE,LC_.*,LINGUAS,TERM,TZ" +RAR_ENV_DELETE_LIST = "PS4,SHELLOPTS,PERLLIB,PERL5LIB,PERL5OPT,PYTHONINSPECT" +RAR_ENV_SET_LIST = "" +RAR_ENV_OVERRIDE_BEHAVIOR = "false" +RAR_AUTHENTICATION = "perform" +RAR_USER_CONSIDERED = "user" +RAR_BOUNDING = "strict" +RAR_WILDCARD_DENIED = "&|" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e05245aa..fda0f7d2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest container: image: xd009642/tarpaulin:develop-nightly - options: --security-opt seccomp=unconfined + options: --security-opt seccomp=unconfined --privileged steps: - name: Checkout code uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index c0bf0047..7a746def 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,7 @@ Cargo.lock # Vagrant *.env -*.vagrant/ \ No newline at end of file +*.vagrant/ + +# Cargo config +.cargo/config.toml \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 76ea54be..a3f110d6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,59 +5,14 @@ "version": "0.2.0", "configurations": [ { - "name": "(gdb) Test", - "type": "cppdbg", - "request": "launch", - "preLaunchTask": "make build_unit_test", - "program": "${workspaceFolder}/bin/unit_test", - "args": [], - "stopAtEntry": false, - "cwd": "${fileDirname}", - "environment": [], - "externalConsole": false, - "MIMode": "gdb", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "Set Disassembly Flavor to Intel", - "text": "-gdb-set disassembly-flavor intel", - "ignoreFailures": true - }, - { "description": "The new process is debugged after a fork. The parent process runs unimpeded.", - "text": "-gdb-set follow-fork-mode child", - "ignoreFailures": true - } - ] - }, - { - "name": "(gdb) Launch", - "type": "cppdbg", + "type": "lldb", "request": "launch", - "preLaunchTask": "setcap", - "program": "/usr/bin/sr", + "name": "Launch", + "program": "${workspaceFolder}/target/debug/sr", "args": ["ls"], - "stopAtEntry": false, - "cwd": "${fileDirname}", - "environment": [], - "externalConsole": false, - "MIMode": "gdb", - "miDebuggerPath": "${workspaceFolder}/.vscode/gdb_root.sh", - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - }, - { - "description": "Set Disassembly Flavor to Intel", - "text": "-gdb-set disassembly-flavor intel", - "ignoreFailures": true - } - ], + "cwd": "${workspaceFolder}" } + + ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ce4d4d7a..c1707c71 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,58 +6,14 @@ } }, "tasks": [ - { - "type": "cppbuild", - "label": "C/C++: gcc build active file", - "command": "/usr/bin/gcc", - "args": [ - "-fdiagnostics-color=always", - "-g", - "${file}", - "-o", - "${fileDirname}/${fileBasenameNoExtension}" - ], - "options": { - "cwd": "${fileDirname}" - }, - "problemMatcher": [ - "$gcc" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "detail": "Task generated by Debugger." - }, - { - "type": "shell", - "label": "make", - "command": "sudo", - "args": [ - "-E", - "/usr/bin/make", - "install" - ], - "options": { - "cwd": "${cwd}" - }, - "problemMatcher": [ - "$gcc" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "detail": "Task generated by Debugger." - }, + { "type": "shell", "label": "setcap", - "dependsOn": "make", "command": "sudo", "args": [ "/usr/bin/setcap", - "=eip", + "=p", "${cwd}/bin/sr" ], "options": { @@ -66,58 +22,6 @@ "group": { "kind": "none" } - }, - { - "type": "shell", - "label": "make build_unit_test", - "command": "sudo", - "args": [ - "/usr/bin/make", - "build_unit_test" - ], - "options": { - "cwd": "${workspaceFolder}", - "env": { - "GDB_DEBUG": "1", - "DEBUG": "1" - } - }, - "problemMatcher": [ - "$gcc" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "detail": "Task generated by Debugger." - }, - { - "type": "shell", - "label": "debug unit_test", - "dependsOn": "make build_unit_test", - "command": "${cwd}/bin/unit_test", - "args": [ - "--debug=gdb" - ], - "options": { - "cwd": "${cwd}" - }, - "isBackground": true, - "problemMatcher": { - "pattern": [ - { - "regexp": ".", - "file": 1, - "location": 2, - "message": 3 - } - ], - "background": { - "activeOnStart": true, - "beginsPattern": ".", - "endsPattern": "Listening on port" - } - } } ], diff --git a/Cargo.toml b/Cargo.toml index 4ae18228..81b83952 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["xtask", "rar-common"] [package] name = "rootasrole" # The project version is managed on json file in resources/rootasrole.json -version = "3.0.6" +version = "3.1.0" rust-version = "1.76.0" authors = ["Eddie Billoir "] edition = "2021" @@ -25,9 +25,17 @@ maintainance ={ status = "actively-maintained", badge = "https://img.shields.io/ [profile.release] strip = "symbols" lto = true -opt-level = "s" +opt-level = 3 codegen-units = 1 +[profile.profiling] +strip = "none" +lto = false +opt-level = 1 +inherits = "release" +debug = true + + #[features] #cursive_lib = [ "cursive" ] #srlibs = [ "pam-client", "bitflags" ] @@ -45,6 +53,7 @@ path = "src/chsr/main.rs" [features] default = ["finder"] finder = ["dep:pcre2", "rar-common/pcre2", "rar-common/finder"] +pcre2 = ["dep:pcre2", "rar-common/pcre2"] [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] } @@ -65,7 +74,7 @@ capctl = "0.2" pcre2 = { version = "0.2", optional = true } serde = { version = "1.0", features=["rc", "derive"] } serde_json = "1.0" -ciborium = "0.2" +cbor4ii = { version = "1.0.0", features = ["serde", "serde1", "use_std"] } glob = "0.3" pam-client2 = "0.5" bitflags = { version = "2.6" } @@ -80,6 +89,9 @@ pest = "2.7" pest_derive = "2.7" const_format = "0.2" hex = "0.4" +bon = "3.5.1" +serde_json_borrow = "0.7.1" +konst = "0.3.16" [dev-dependencies] log = "0.4" diff --git a/README.md b/README.md index 5ad145ef..6e6d62d0 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ -# RootAsRole (V3.0.6) : A memory-safe and security-oriented alternative to sudo/su commands +# RootAsRole (V3.1.0) : A memory-safe and security-oriented alternative to sudo/su commands **RootAsRole** is a project to allow Linux/Unix administrators to delegate their administrative tasks access rights to users. Its main features are : @@ -29,13 +29,15 @@ * File relocation ability. * Multi-layered and inheritable execution environment configuration. * Interoperable and evolvable by using [JSON](https://www.json.org/) as the main configuration file format. + * Interchangeable file format with [JSON5](https://www.json.org/) and [CBOR](https://cbor.io/) for performance and human readability. + * Setuid managed by set of users or (set-of-)groups (all-and-deny, or nothing-then-grant), Thanks to @[hocineait7](https://github.com/hocineait7). * Command matching based on commonly-used open-source libraries: - * [glob](https://docs.rs/glob/latest/glob/) for binary path + * [glob](https://docs.rs/glob/latest/glob/) for binary pathv * [PCRE2](https://www.pcre.org/) for command arguments If you need help to configure a RootAsRole policy, you can use our **[capable tool](https://github.com/LeChatP/RootAsRole-capable)**. This tool identifies the rights required by specific commands, making it easier to define a precise policy. -For administrators who already use **Ansible playbooks** for their tasks and wish to implement **RootAsRole**, our tool [gensr](https://github.com/LeChatP/RootAsRole-utils) can generate an initial draft of a **RootAsRole policy**. The `gensr` tool works by running your Ansible playbook alongside the [capable tool](https://github.com/LeChatP/RootAsRole-capable), creating a draft policy based on the observed required rights. This process helps administrators to harden their Ansible tasks. It helps to verify eventual third-party supply-chain attacks. +For administrators who already use **Ansible playbooks** for their tasks and wish to implement RootAsRole, our tool [gensr](https://github.com/LeChatP/RootAsRole-utils) can generate an initial draft of a RootAsRole policy. The `gensr` tool works by running your Ansible playbook alongside the [capable tool](https://github.com/LeChatP/RootAsRole-capable), creating a draft policy based on the observed required rights. This process helps administrators to harden their Ansible tasks. It helps to verify eventual **third-party supply-chain attacks**. **Note:** The `gensr` tool is still in development and may not work with all playbooks. If you wish to contribute to this project, feel free to make issues and pull requests. @@ -105,6 +107,9 @@ Execute privileged commands with a role-based access control system Options: -r, --role <ROLE> Role to select -t, --task <TASK> Task to select (--role required) + -u, --user <USER> User to execute the command as + -g, --group <GROUP<,GROUP...>> Group(s) to execute the command as + -E, --preserve-env Keep environment variables from the current process -p, --prompt <PROMPT> Prompt to display -i, --info Display rights of executor -h, --help Print help (see more with '--help') @@ -116,7 +121,22 @@ If you're accustomed to utilizing the sudo tool and find it difficult to break t alias sudo="sr" ``` -However you won't find out exact same options as sudo, you can use the `--role` option to specify the role you want to use instead. +## Performance outperforms `sudo` (and `sudo-rs`) command + +[![Performance comparison](https://github.com/LeChatP/RaR-perf/raw/main/result_25-07-04_15.44.png)](https://github.com/LeChatP/RaR-perf) + +Since RootAsRole 3.1.0, the project introduced CBOR file format, consequently the performance of the `sr` command has been significantly improved. The new version now outperforms the `sudo` command by a raw 77% (with 1 rule each side), and more you add rules, more the performance gap increases. The slope between the `sudo` and `sr` commands is 40% better, meaning that the more rules you add, the more the `sr` command will outperform the `sudo` command. You can reproduce this performance test by following the [RaR-perf](https://github.com/LeChatP/RaR-perf) repository guideline. + +The performance of `sudo-rs` are actually even-or-worse than `sudo` command for the few tests I was able to do. However, the sudo-rs project is crashing when you try to add more than 100 rules. [I created an issue on their repository, but it's tagged as won't fix](https://github.com/trifectatechfoundation/sudo-rs/issues/1192). + +But that is not all, as we wish to introduce RDBMS (Relational Database Management System) support in the future (with Limbo SQLite and regular DBMS solutions), the performance will be even better. + + +### Why Performance Matters + +When it comes to managing infrastructure with tools like Ansible, executing privileged commands will become a common task, so you multiply the number of commands executed by the number of rules. With the `sudo` command, it didn't matters as long you had only one rule everywhere, but now with RootAsRole, you can have a lot of rules as long you configure it with `gensr` --- generating a first version of a very-specific policy --- thus increasing the number of rules inside the policy, so the performance of the `sr` command now matters. + +With RootAsRole, you add more access control rules, enforcing a better POLP, without sacrificing performance. ## Why do you need this tool ? diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index af24bd39..9916d87b 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -23,6 +23,7 @@ # Reference Guide - [Configure RootAsRole](chsr/file-config.md) +- [File Config Conversion](chsr/convert.md) - [Continuous Integration](continuous-integration.md) - [How to contribute](dev/CONTRIBUTE.md) - [FAQ](faq.md) diff --git a/book/src/chsr/README.md b/book/src/chsr/README.md index 9b8df0a8..9024d5a3 100644 --- a/book/src/chsr/README.md +++ b/book/src/chsr/README.md @@ -104,4 +104,11 @@ chsr options timeout [operation] del [items,...] Remove items from the list. set [items,...] Set items in the list. purge Remove all items from the list. - + +Convert policy format : +chsr convert (-r) (--from [from_type] [from_file]) [to_type] [to_file] +Supported types: json, cbor + -r, --reconfigure Reconfigure /etc/security/rootasrole.json file to specify the new location. + --from [from_type] [from_file] Specify the type and file to convert from. + Warning: the new location should be under a protected directory. + \ No newline at end of file diff --git a/book/src/chsr/convert.md b/book/src/chsr/convert.md new file mode 100644 index 00000000..82a37345 --- /dev/null +++ b/book/src/chsr/convert.md @@ -0,0 +1,15 @@ +# File Config Conversion + +## Converting JSON to CBOR and vice versa + +Converting the `/etc/security/rootasrole.json` file to CBOR format (and configure the policy to the new location with `-r` option) : + +`chsr convert -r cbor /etc/security/rootasrole.bin` + +This command will read the JSON file, convert it to CBOR format, and save it to `/etc/security/rootasrole.bin`. The `-r` option changes the file `/etc/security/rootasrole.json` to specify the new location in `path` field of the configuration file. + +To convert the CBOR file back to JSON format, you can use the following command: + +`chsr convert -r json /etc/security/rootasrole.json` + +This command will read the CBOR file, convert it back to JSON format, and save it to `/etc/security/rootasrole.json`. The `-r` option changes the file `/etc/security/rootasrole.json` to specify the new location in `path` field of the configuration file. \ No newline at end of file diff --git a/book/src/chsr/file-config.md b/book/src/chsr/file-config.md index b85d3682..b2f1860c 100644 --- a/book/src/chsr/file-config.md +++ b/book/src/chsr/file-config.md @@ -22,15 +22,15 @@ The following example shows a RootAsRole config without plugins when almost ever ```json { - "version": "3.0.0-alpha.4", // Version of the configuration file + "version": "3.1.0", // Version of the configuration file "storage": { // Storage settings, Roles storage location "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 + "path": "/etc/security/rootasrole.json" // Path to the storage file } }, - "options": { + "options": { // Global options "path": { // Path options "default": "delete", // Default policy for path, delete, keep-safe, keep-unsafe, inherit "add": [ // Paths to add to the whitelist @@ -75,12 +75,12 @@ The following example shows a RootAsRole config without plugins when almost ever "type": "user" // Type of actor: user, group }, { - "groups": 0, // ID of the group or a list of ID for AND condition + "name": "root", // ID of the group or a list of ID for AND condition "type": "group" }, { "type": "group", - "groups": [ // List of groups, this is an AND condition between groups + "names": [ // List of groups, this is an AND condition between groups "groupA", "groupB" ] @@ -124,24 +124,24 @@ The following example shows a RootAsRole config without plugins when almost ever "commands": { "default": "all", // Default policy for commands, allow-all, deny-all "add": [ // Commands to add to the whitelist - "ls", - "echo" + "/bin/ls -al", // specify the full path to the command and its arguments + "/bin/echo" ], "sub": [ // Commands to add to the blacklist - "cat", - "grep" + "/bin/cat", + "/bin/grep" ] }, "options": { // Task-level options "path": { "default": "delete", // When default is not inherit, all upper level options are ignored "add": [ - "path1", - "path2" + "/usr/bin", + "/usr/sbin" ], "sub": [ - "path3", - "path4" + "/usr/bin/somepath", + "/usr/sbin/somepath" ] }, "env": { @@ -157,7 +157,11 @@ The following example shows a RootAsRole config without plugins when almost ever "delete": [ "env5", "env6" - ] + ], + "set": { + "env7": "value7", // Set environment variable env7 to value7 + "env8": "value8" // Set environment variable env8 to value8 + } }, "root": "privileged", "bounding": "ignore", @@ -217,7 +221,7 @@ The following example shows a RootAsRole config using role hierarchy plugin. ```json { - "version": "3.0.0-alpha.4", + "version": "3.1.0", "roles": [ { "parents": ["user"], @@ -265,7 +269,7 @@ The following example shows a RootAsRole config using separation of duties plugi ```json { - "version": "3.0.0-alpha.4", + "version": "3.1.0", "roles": [ { "ssd": ["user"], @@ -313,7 +317,7 @@ Hashchecker plugin verifies the integrity of the binary before executing it. The ```json { - "version": "3.0.0-alpha.4", + "version": "3.1.0", "roles": [ { "name": "admin", @@ -664,4 +668,4 @@ The `check` list is a list of environment variables that will be checked for uns ## What are dbus and file credentials fields? -the `dbus` and `file` fields are used for gensr tool from RootAsRole-utils repository. They are enforced to the DBus and file permissions. The `dbus` field is used to allow DBus methods. The `file` field is used to allow file permissions. The gensr tool will generate the DBus and file permissions in according to the `setuid` credentials. So gensr tool requires the `setuid` field to be set. \ No newline at end of file +the `dbus` and `file` fields are used for gensr tool from RootAsRole-gensr repository. They are enforced to the DBus and file permissions. The `dbus` field is used to allow DBus methods. The `file` field is used to allow file permissions. The gensr tool will generate the DBus and file permissions in according to the `setuid` credentials. So gensr tool requires the `setuid` field to be set. \ No newline at end of file diff --git a/book/src/misc/contributors.md b/book/src/misc/contributors.md index 58196e28..caa8f4e0 100644 --- a/book/src/misc/contributors.md +++ b/book/src/misc/contributors.md @@ -2,12 +2,10 @@ Eddie Billoir : -Ahmad Samer Wazan : +Ahmad Samer Wazan : -Rémi Venant : +Rémi Venant: Guillaume Daumas : -Anderson Hemlee : - -Romain Laborde : \ No newline at end of file +Romain Laborde : \ No newline at end of file diff --git a/book/src/sr/README.md b/book/src/sr/README.md index fe1d1620..33fedd3a 100644 --- a/book/src/sr/README.md +++ b/book/src/sr/README.md @@ -14,6 +14,7 @@ -r, --role <ROLE> Role to select -t, --task <TASK> Task to select (--role required) -u, --user <USER> Specify the user to execute the command as + -g, --group <GROUP(,GROUP...)> Specify the group to execute the command as -E, --preserve-env Preserve environment variables if allowed by a matching task -p, --prompt <PROMPT> Prompt to display -i, --info Display rights of executor diff --git a/build.rs b/build.rs index e8327bb5..87d750c2 100644 --- a/build.rs +++ b/build.rs @@ -16,17 +16,6 @@ fn package_version>(path: P) -> Result> { .expect("Failed to get package version")) } -fn write_version<'a>(f: &'a mut File, package_version: &'a str) -> Result<&'a str, Box> { - f.write_all( - format!( - "pub const PACKAGE_VERSION: &str = \"{}\";\n", - package_version - ) - .as_bytes(), - )?; - Ok(package_version) -} - fn set_cargo_version(package_version: &str, file: &str) -> Result<(), Box> { let cargo_toml = File::open(std::path::Path::new(file)).expect("Cargo.toml not found"); let reader = BufReader::new(cargo_toml); @@ -76,23 +65,6 @@ fn main() { return; } let package_version = package_version("Cargo.toml").expect("Failed to get package version"); - let dest_path = std::path::Path::new("rar-common") - .join("src") - .join("version.rs"); - if dest_path.exists() - && fs::read_to_string(&dest_path) - .unwrap() - .ends_with(&format!("\"{}\";\n", package_version)) - { - return; - } - let mut f = File::create(dest_path).unwrap(); - f.write_all(b"// This file is generated by build.rs\n") - .unwrap(); - f.write_all(b"// Do not edit this file directly\n").unwrap(); - f.write_all(b"// Instead edit build.rs and run cargo build\n") - .unwrap(); - write_version(&mut f, &package_version).expect("Failed to write version"); if let Err(err) = set_cargo_version(&package_version, "rar-common/Cargo.toml") { eprintln!("cargo:warning={}", err); diff --git a/rar-common/Cargo.toml b/rar-common/Cargo.toml index ec0df3d3..74f7870d 100644 --- a/rar-common/Cargo.toml +++ b/rar-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rootasrole-core" -version = "3.0.6" +version = "3.1.0" edition = "2021" description = "This core crate contains the RBAC and main features for the RootAsRole project." license = "LGPL-3.0-or-later" @@ -27,12 +27,18 @@ log = "0.4" syslog = "7.0" env_logger = "0.11" bon = { version = "3.3.2", features = ["experimental-overwritable"] } +cbor4ii = { version = "1.0.0", features = ["serde", "serde1", "use_std"] } +konst = "0.3.16" [dev-dependencies] log = "0.4" env_logger = "0.11" test-log = { version = "0.2" } +[build-dependencies] +serde = { version = "1.0.210", features=["rc", "derive"] } +serde_json = "1.0.132" + [features] pcre2 = ["dep:pcre2"] finder = ["dep:glob"] diff --git a/rar-common/src/api.rs b/rar-common/src/api.rs index c25a6ddd..9b7102d4 100644 --- a/rar-common/src/api.rs +++ b/rar-common/src/api.rs @@ -10,6 +10,7 @@ use strum::EnumIs; #[cfg(feature = "finder")] use crate::database::finder::{ActorMatchMin, Cred, ExecSettings, TaskMatch}; +#[cfg(feature = "finder")] use crate::database::FilterMatcher; use crate::database::{ diff --git a/rar-common/src/config.rs b/rar-common/src/config.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/rar-common/src/database/actor.rs b/rar-common/src/database/actor.rs index b8007b55..aeb9e8af 100644 --- a/rar-common/src/database/actor.rs +++ b/rar-common/src/database/actor.rs @@ -1,15 +1,15 @@ -use std::fmt::{self, Formatter}; +use std::{ + borrow::Cow, + fmt::{self, Display, Formatter}, +}; use bon::bon; use nix::unistd::{Group, User}; -use serde::{ - de::{self, Visitor}, - Deserialize, Deserializer, Serialize, -}; +use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use strum::EnumIs; -#[derive(Serialize, Debug, EnumIs, Clone, PartialEq, Eq)] +#[derive(Serialize, Debug, EnumIs, Clone, PartialEq, Eq, strum::Display)] #[serde(untagged, rename_all = "lowercase")] pub enum SGenericActorType { Id(u32), @@ -19,8 +19,19 @@ pub enum SGenericActorType { #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct SUserType(SGenericActorType); +#[derive(Deserialize, Serialize, Debug, EnumIs, Clone, PartialEq, Eq, strum::Display)] +#[serde(untagged, rename_all = "lowercase")] +pub enum DGenericActorType<'a> { + Id(u32), + #[serde(borrow)] + Name(Cow<'a, str>), +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub struct DUserType<'a>(#[serde(borrow)] DGenericActorType<'a>); + impl SUserType { - pub(super) fn fetch_id(&self) -> Option { + pub fn fetch_id(&self) -> Option { match &self.0 { SGenericActorType::Id(id) => Some(*id), SGenericActorType::Name(name) => match User::from_name(name) { @@ -45,6 +56,18 @@ impl SUserType { } } +impl DUserType<'_> { + pub fn fetch_id(&self) -> Option { + match &self.0 { + DGenericActorType::Id(id) => Some(*id), + DGenericActorType::Name(name) => match User::from_name(name) { + Ok(Some(user)) => Some(user.uid.as_raw()), + _ => None, + }, + } + } +} + impl fmt::Display for SUserType { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self.0 { @@ -57,6 +80,9 @@ impl fmt::Display for SUserType { #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct SGroupType(SGenericActorType); +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] +pub struct DGroupType<'a>(#[serde(borrow)] DGenericActorType<'a>); + impl fmt::Display for SGroupType { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self.0 { @@ -92,22 +118,66 @@ impl SGroupType { } } -impl std::fmt::Display for SGenericActorType { +impl DGroupType<'_> { + pub fn fetch_id(&self) -> Option { + match &self.0 { + DGenericActorType::Id(id) => Some(*id), + DGenericActorType::Name(name) => match Group::from_name(name) { + Ok(Some(group)) => Some(group.gid.as_raw()), + _ => None, + }, + } + } +} + +impl Display for DGroupType<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - SGenericActorType::Id(id) => write!(f, "{}", id), - SGenericActorType::Name(name) => write!(f, "{}", name), + match &self.0 { + DGenericActorType::Id(id) => write!(f, "{}", id), + DGenericActorType::Name(name) => write!(f, "{}", name), + } + } +} +impl Display for DUserType<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match &self.0 { + DGenericActorType::Id(id) => write!(f, "{}", id), + DGenericActorType::Name(name) => write!(f, "{}", name), } } } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, EnumIs)] +#[derive(Serialize, PartialEq, Eq, Debug, Clone, EnumIs)] #[serde(untagged)] pub enum SGroups { Single(SGroupType), Multiple(Vec), } +impl Display for SGroups { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + SGroups::Single(group) => write!(f, "[{}]", group), + SGroups::Multiple(groups) => { + let mut result = String::new(); + for group in groups { + result.push_str(&format!("{}, ", group)); + } + result.pop(); // Remove last comma + result.pop(); // Remove last space + write!(f, "[{}]", result) + } + } + } +} + +#[derive(Serialize, PartialEq, Eq, Debug, Clone, EnumIs, strum::Display)] +#[serde(untagged)] +pub enum DGroups<'a> { + Single(#[serde(borrow)] DGroupType<'a>), + Multiple(#[serde(borrow)] Cow<'a, [DGroupType<'a>]>), +} + impl SGroups { pub fn len(&self) -> usize { match self { @@ -130,40 +200,133 @@ impl SGroups { } } -impl<'de> Deserialize<'de> for SGenericActorType { +impl DGroups<'_> { + pub fn len(&self) -> usize { + match self { + DGroups::Single(_) => 1, + DGroups::Multiple(groups) => groups.len(), + } + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl<'de: 'a, 'a> Deserialize<'de> for DGroups<'a> { fn deserialize(deserializer: D) -> Result where - D: Deserializer<'de>, + D: serde::Deserializer<'de>, { - struct IdVisitor; + struct DGroupsVisitor<'a> { + marker: std::marker::PhantomData<&'a ()>, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for DGroupsVisitor<'a> { + type Value = DGroups<'a>; - impl<'de> Visitor<'de> for IdVisitor { - type Value = SGenericActorType; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") + } - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("user ID as a number or string") + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + if let Ok(group) = v.parse() { + return Ok(DGroups::Single(DGroupType(DGenericActorType::Id(group)))); + } else { + return Ok(DGroups::Single(DGroupType(DGenericActorType::Name( + Cow::Borrowed(v), + )))); + } } - fn visit_u32(self, id: u32) -> Result + fn visit_u64(self, value: u64) -> Result where - E: de::Error, + E: serde::de::Error, { - Ok(SGenericActorType::Id(id)) + if value > u32::MAX as u64 { + return Err(E::custom("value is too large")); + } + Ok(DGroups::Single(DGroupType(DGenericActorType::Id( + value as u32, + )))) } - fn visit_str(self, id: &str) -> Result + fn visit_seq(self, mut seq: A) -> Result where - E: de::Error, + A: serde::de::SeqAccess<'de>, { - let rid: Result = id.parse(); - match rid { - Ok(id) => Ok(SGenericActorType::Id(id)), - Err(_) => Ok(SGenericActorType::Name(id.to_string())), + let mut groups = Vec::new(); + while let Some(group) = seq.next_element::>()? { + groups.push(group); + } + if groups.len() == 1 { + Ok(DGroups::Single(groups.remove(0))) + } else { + Ok(DGroups::Multiple(Cow::Owned(groups))) } } } + deserializer.deserialize_any(DGroupsVisitor { + marker: std::marker::PhantomData, + }) + } +} - deserializer.deserialize_any(IdVisitor) +impl<'de: 'a, 'a> Deserialize<'de> for SGroups { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SGroupsVisitor; + impl<'de: 'a, 'a> serde::de::Visitor<'de> for SGroupsVisitor { + type Value = SGroups; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + if let Ok(group) = v.parse() { + return Ok(SGroups::Single(SGroupType(SGenericActorType::Id(group)))); + } else { + return Ok(SGroups::Single(SGroupType(SGenericActorType::Name( + v.to_string(), + )))); + } + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + if value > u32::MAX as u64 { + return Err(E::custom("value is too large")); + } + Ok(SGroups::Single(SGroupType(SGenericActorType::Id( + value as u32, + )))) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut groups = Vec::new(); + while let Some(group) = seq.next_element::()? { + groups.push(group); + } + if groups.len() == 1 { + Ok(SGroups::Single(groups.remove(0))) + } else { + Ok(SGroups::Multiple(groups)) + } + } + } + deserializer.deserialize_any(SGroupsVisitor) } } @@ -185,12 +348,48 @@ impl From<&str> for SUserType { } } +impl<'a> From<&'a str> for DUserType<'a> { + fn from(name: &'a str) -> Self { + DUserType(name.into()) + } +} + +impl<'a> From for DUserType<'a> { + fn from(name: String) -> Self { + DUserType(DGenericActorType::Name(name.into())) + } +} + +impl<'a> From<&'a str> for DGroupType<'a> { + fn from(name: &'a str) -> Self { + DGroupType(name.into()) + } +} + +impl From for DUserType<'_> { + fn from(id: u32) -> Self { + DUserType(id.into()) + } +} + +impl From for DGroupType<'_> { + fn from(id: u32) -> Self { + DGroupType(id.into()) + } +} + impl From<&str> for SGroupType { fn from(name: &str) -> Self { SGroupType(name.into()) } } +impl<'a> From> for DGroupType<'a> { + fn from(name: Cow<'a, str>) -> Self { + DGroupType(DGenericActorType::Name(name)) + } +} + impl From for SGroupType { fn from(group: Group) -> Self { SGroupType(SGenericActorType::Id(group.gid.as_raw())) @@ -203,6 +402,22 @@ impl From<&str> for SGenericActorType { } } +impl<'a> From<&'a str> for DGenericActorType<'a> { + fn from(name: &'a str) -> Self { + if name.parse::().is_ok() { + DGenericActorType::Id(name.parse().unwrap()) + } else { + DGenericActorType::Name(Cow::Borrowed(name)) + } + } +} + +impl<'a> From for DGenericActorType<'a> { + fn from(name: u32) -> Self { + DGenericActorType::Id(name) + } +} + impl From for SGenericActorType { fn from(id: u32) -> Self { SGenericActorType::Id(id) @@ -219,6 +434,16 @@ impl PartialEq for SUserType { } } +impl PartialEq for DUserType<'_> { + fn eq(&self, other: &User) -> bool { + let uid = self.fetch_id(); + match uid { + Some(uid) => uid == other.uid.as_raw(), + None => false, + } + } +} + impl PartialEq for SUserType { fn eq(&self, other: &str) -> bool { self.eq(&SUserType::from(other)) @@ -237,6 +462,12 @@ impl PartialEq for SUserType { } } +impl PartialEq for DUserType<'_> { + fn eq(&self, other: &u32) -> bool { + self.eq(&DUserType::from(*other)) + } +} + impl PartialEq for SGroupType { fn eq(&self, other: &u32) -> bool { self.eq(&SGroupType::from(*other)) @@ -253,23 +484,12 @@ impl PartialEq for SGroupType { } } -impl PartialEq<[SGroupType; N]> for SGroups { - fn eq(&self, other: &[SGroupType; N]) -> bool { - match self { - SGroups::Single(group) => { - if N == 1 { - group == &other[0] - } else { - false - } - } - SGroups::Multiple(groups) => { - if groups.len() == N { - groups.iter().zip(other.iter()).all(|(a, b)| a == b) - } else { - false - } - } +impl PartialEq for DGroupType<'_> { + fn eq(&self, other: &Group) -> bool { + let gid = self.fetch_id(); + match gid { + Some(gid) => gid == other.gid.as_raw(), + None => false, } } } @@ -284,18 +504,48 @@ impl From<[SGroupType; N]> for SGroups { } } -impl FromIterator for SGroups { - fn from_iter>(iter: I) -> Self { - let mut iter = iter.into_iter(); - let first = iter.next().unwrap(); - let mut groups: Vec = vec![first.as_str().into()]; - for group in iter { - groups.push(group.as_str().into()); +impl TryInto> for &DGroups<'_> { + type Error = String; + + fn try_into(self) -> Result, Self::Error> { + match self { + DGroups::Single(group) => Ok(vec![group + .fetch_id() + .ok_or(format!("{} group does not exist", group))?]), + DGroups::Multiple(groups) => { + let mut ids = Vec::new(); + for group in groups.iter() { + ids.push( + group + .fetch_id() + .ok_or(format!("{} group does not exist", group))?, + ); + } + Ok(ids) + } } - if groups.len() == 1 { - SGroups::Single(groups[0].to_owned()) - } else { - SGroups::Multiple(groups) + } +} + +impl TryInto> for SGroups { + type Error = String; + + fn try_into(self) -> Result, Self::Error> { + match self { + SGroups::Single(group) => Ok(vec![group + .fetch_id() + .ok_or(format!("{} group does not exist", group))?]), + SGroups::Multiple(groups) => { + let mut ids = Vec::new(); + for group in groups { + ids.push( + group + .fetch_id() + .ok_or(format!("{} group does not exist", group))?, + ); + } + Ok(ids) + } } } } @@ -330,6 +580,22 @@ impl From> for SGroups { } } +impl<'a> From>> for DGroups<'a> { + fn from(groups: Vec>) -> Self { + if groups.len() == 1 { + DGroups::Single(groups[0].clone()) + } else { + DGroups::Multiple(Cow::Owned(groups)) + } + } +} + +impl<'a> From> for DGroups<'a> { + fn from(groups: DGroupType<'a>) -> Self { + DGroups::Single(groups) + } +} + impl From for SGroups { fn from(group: u32) -> Self { SGroups::Single(group.into()) @@ -360,19 +626,6 @@ impl PartialEq> for SGroups { } } -impl core::fmt::Display for SGroups { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - SGroups::Single(group) => { - write!(f, "{}", group) - } - SGroups::Multiple(groups) => { - write!(f, "{:?}", groups) - } - } - } -} - #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs)] #[serde(tag = "type", rename_all = "lowercase")] pub enum SActor { @@ -385,7 +638,11 @@ pub enum SActor { }, #[serde(rename = "group")] Group { - #[serde(alias = "names", skip_serializing_if = "Option::is_none")] + #[serde( + alias = "names", + alias = "name", + skip_serializing_if = "Option::is_none" + )] groups: Option, #[serde(default, flatten)] _extra_fields: Map, @@ -394,6 +651,25 @@ pub enum SActor { Unknown(Value), } +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, strum::Display)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum DActor<'a> { + #[serde(rename = "user")] + #[strum(to_string = "User {id}")] + User { + #[serde(borrow, alias = "name")] + id: DUserType<'a>, + }, + #[serde(rename = "group")] + #[strum(to_string = "Group {groups}")] + Group { + #[serde(borrow, alias = "names", alias = "name", alias = "id")] + groups: DGroups<'a>, + }, + #[serde(untagged)] + Unknown(Value), +} + #[bon] impl SActor { #[builder(finish_fn = build)] @@ -438,6 +714,8 @@ impl core::fmt::Display for SActor { } #[cfg(test)] mod tests { + use nix::unistd::getuid; + use super::*; #[test] @@ -455,6 +733,14 @@ mod tests { let group = SGroupType::from(0); assert_eq!(group.fetch_id(), Some(0)); + let user = SUserType::from("root"); + assert_eq!(user.fetch_id(), Some(0)); + + let group = SGroupType::from("root"); + assert_eq!(group.fetch_id(), Some(0)); + + let group = SGroupType::from("unkown"); + assert_eq!(group.fetch_id(), None); } #[test] fn test_fetch_user() { @@ -466,6 +752,9 @@ mod tests { #[test] fn test_sgroups_multiple() { + let groups = SGroups::from(0); + + assert_eq!(groups.len(), 1); let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(200)]); assert_eq!(groups.len(), 2); @@ -507,4 +796,221 @@ mod tests { assert!(!group1.fetch_eq(&group2)); } + + #[test] + fn test_duser_type_creation() { + let user_by_id = DUserType::from(0); + let user_by_name = DUserType::from("testuser"); + + assert_eq!(user_by_id.to_string(), "0"); + assert_eq!(user_by_name.to_string(), "testuser"); + } + #[test] + fn test_fetch_did() { + let user = DUserType::from(0); + assert_eq!(user.fetch_id(), Some(0)); + + let group = DGroupType::from(0); + assert_eq!(group.fetch_id(), Some(0)); + let user = DUserType::from("root"); + assert_eq!(user.fetch_id(), Some(0)); + + let group = DGroupType::from("root"); + assert_eq!(group.fetch_id(), Some(0)); + + let group = DGroupType::from("unkown"); + assert_eq!(group.fetch_id(), None); + } + + #[test] + fn test_dgroups_single() { + let groups = DGroups::from(DGroupType::from(0)); + + assert_eq!(groups.len(), 1); + assert!(!groups.is_empty()); + + if let DGroups::Single(group_list) = groups { + assert_eq!(group_list.to_string(), "0"); + } else { + panic!("Expected SGroups::Single"); + } + } + + #[test] + fn test_is_dempty() { + let groups = DGroups::Multiple(Cow::Borrowed(&[])); + assert!(groups.is_empty()); + } + + #[test] + fn test_sactor_display() { + let user = SActor::User { + id: Some(SUserType::from(0)), + _extra_fields: Map::new(), + }; + let group = SActor::Group { + groups: Some(SGroups::from(vec![SGroupType::from(0)])), + _extra_fields: Map::new(), + }; + assert_eq!(user.to_string(), "User: 0"); + assert_eq!(group.to_string(), "Group: [0]"); + let group = SActor::Group { + groups: Some(SGroups::from(vec![ + SGroupType::from(0), + SGroupType::from("test"), + ])), + _extra_fields: Map::new(), + }; + assert_eq!(group.to_string(), "Group: [0, test]"); + let unknown = SActor::Unknown(Value::String("unknown".to_string())); + assert_eq!(unknown.to_string(), "Unknown: \"unknown\""); + } + + #[test] + fn test_display_dgrouptype() { + let group = DGroupType::from("test"); + assert_eq!(group.to_string(), "test"); + } + + #[test] + fn test_partialeq_sgroups() { + let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from("test")]); + let other_groups = vec![SGroupType::from(0), SGroupType::from("test")]; + assert_eq!(groups, other_groups); + let other_groups = vec![SGroupType::from(0), SGroupType::from("test2")]; + assert_ne!(groups, other_groups); + let other_groups = vec![SGroupType::from(0)]; + assert_ne!(groups, other_groups); + let other_groups = vec![ + SGroupType::from(0), + SGroupType::from("test"), + SGroupType::from("test2"), + ]; + assert_ne!(groups, other_groups); + let groups = SGroups::from(0); + let other_groups = vec![SGroupType::from(0)]; + assert_eq!(groups, other_groups); + let other_groups = vec![SGroupType::from(0), SGroupType::from("test")]; + assert_ne!(groups, other_groups); + } + + #[test] + fn test_sfetcheq_group() { + let group1 = SGroupType::from(0); + let group2 = SGroupType::from(0); + + assert!(group1.fetch_eq(&group2)); + let group2 = SGroupType::from("root"); + assert!(group1.fetch_eq(&group2)); + let group2 = SGroupType::from("unkown"); + assert!(!group1.fetch_eq(&group2)); + + let groups = SGroups::from(vec![ + SGroupType::from(0), + SGroupType::from(getuid().as_raw() + 1), + ]); + let other_groups = SGroups::from(vec![ + SGroupType::from(0), + SGroupType::from(getuid().as_raw() + 1), + ]); + assert!(groups.fetch_eq(&other_groups)); + let other_groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from("test2")]); + assert!(!groups.fetch_eq(&other_groups)); + let other_groups = SGroups::from(0); + assert!(!groups.fetch_eq(&other_groups)); + let groups = SGroups::from(0); + assert!(groups.fetch_eq(&other_groups)); + } + + #[test] + fn test_sfetcheq_user() { + let user1 = SUserType::from(0); + let user2 = SUserType::from(0); + + assert!(user1.fetch_eq(&user2)); + let user2 = SUserType::from("root"); + assert!(user1.fetch_eq(&user2)); + let user2 = SUserType::from("unkown"); + assert!(!user1.fetch_eq(&user2)); + } + + #[test] + fn test_from() { + let cow = Cow::Borrowed("test"); + let group = DGroupType::from(cow); + assert_eq!(group.to_string(), "test"); + let group = Group::from_gid(0.into()).unwrap().unwrap(); + let group = SGroupType::from(group); + assert_eq!(group.fetch_id(), Some(0)); + let group = SGroups::from([SGroupType::from(0)]); + assert!(group.is_single()); + let group = SGroups::from(["test"]); + assert!(group.is_single()); + let group = SGroups::from(["test", "test2"]); + assert!(!group.is_single()); + let group = SGroups::from(vec![0, 1]); + assert!(!group.is_single()); + let group = SGroups::from(vec![0]); + assert!(group.is_single()); + } + + #[test] + fn test_partialeq_user() { + assert!(SUserType::from(0) == 0); + assert!(SUserType::from(0) != 1); + assert!(DUserType::from(0) == 0); + assert!(DUserType::from(0) != 1); + let user = User::from_uid(0.into()).unwrap().unwrap(); + assert!(SUserType::from(0) == user); + assert!(SUserType::from(0) != 1); + assert!(DUserType::from(0) == user); + assert!(DUserType::from(0) != 1); + assert!(SUserType::from("root") == user); + assert!(SUserType::from("test") != user); + assert!(DUserType::from("root") == user); + assert!(DUserType::from("test") != user); + } + + #[test] + fn test_partialeq_group() { + let group = Group::from_gid(0.into()).unwrap().unwrap(); + assert!(SGroupType::from(0) == group); + assert!(SGroupType::from(1) != group); + assert!(SGroupType::from("root") == group); + assert!(SGroupType::from("test") != group); + assert!(DGroupType::from(0) == group); + assert!(DGroupType::from(1) != group); + assert!(DGroupType::from("root") == group); + assert!(DGroupType::from("test") != group); + } + + #[test] + fn test_tryinto_sgroups() { + let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(1)]); + let ids: Vec = groups.try_into().unwrap(); + assert_eq!(ids, vec![0, 1]); + + let groups = SGroups::from(vec![SGroupType::from(0)]); + let ids: Vec = groups.try_into().unwrap(); + assert_eq!(ids, vec![0]); + + let groups = SGroups::from(vec![SGroupType::from("unkown")]); + let ids: Result, _> = groups.try_into(); + assert!(ids.is_err()); + } + + #[test] + fn test_tryinto_dgroups() { + let groups: DGroups<'_> = DGroups::from(vec![0.into(), 1.into()]); + let ids: Vec = (&groups).try_into().unwrap(); + assert_eq!(ids, vec![0, 1]); + + let groups = DGroups::from(vec![DGroupType::from(0)]); + let ids: Vec = (&groups).try_into().unwrap(); + assert_eq!(ids, vec![0]); + + let groups = DGroups::from(vec![DGroupType::from("unkown")]); + let ids: Result, _> = (&groups).try_into(); + assert!(ids.is_err()); + } } diff --git a/rar-common/src/database/de.rs b/rar-common/src/database/de.rs new file mode 100644 index 00000000..43e3f29f --- /dev/null +++ b/rar-common/src/database/de.rs @@ -0,0 +1,425 @@ +use core::fmt; + +use serde::Deserialize; + +use crate::database::structs::{SCommand, SetBehavior}; + +use super::{ + actor::SGenericActorType, + structs::{SCapabilities, SCommands}, +}; +use capctl::CapSet; +use serde::{ + de::{self, MapAccess, SeqAccess, Visitor}, + Deserializer, +}; +use serde_json::Map; +use strum::Display; + +impl<'de> Deserialize<'de> for SetBehavior { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SetBehaviorVisitor; + + impl<'de> Visitor<'de> for SetBehaviorVisitor { + type Value = SetBehavior; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.parse().map_err(de::Error::custom) + } + fn visit_i32(self, v: i32) -> Result + where + E: de::Error, + { + if v > 1 || v < 0 { + return Err(de::Error::custom(format!( + "Invalid value for SetBehavior: {}", + v + ))); + } + SetBehavior::from_repr(v as u8).ok_or(de::Error::custom(format!( + "Invalid value for SetBehavior: {}", + v + ))) + } + fn visit_u8(self, v: u8) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_u16(self, v: u16) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_u32(self, v: u32) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + if v > i32::MAX as u64 { + return Err(de::Error::custom(format!( + "Invalid value for SetBehavior: {}", + v + ))); + } + self.visit_i32(v as i32) + } + fn visit_i8(self, v: i8) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_i16(self, v: i16) -> Result + where + E: de::Error, + { + self.visit_i32(v as i32) + } + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + if v > i32::MAX as i64 || v < i32::MIN as i64 { + return Err(de::Error::custom(format!( + "Invalid value for SetBehavior: {}", + v + ))); + } + self.visit_i32(v as i32) + } + } + + deserializer.deserialize_any(SetBehaviorVisitor) + } +} + +impl<'de> Deserialize<'de> for SCapabilities { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SCapabilitiesVisitor; + + #[derive(Deserialize, Display)] + #[serde(rename_all = "kebab-case")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "a")] + Add, + #[serde(alias = "del", alias = "s")] + Sub, + #[serde(untagged)] + Other(String), + } + + impl<'de> Visitor<'de> for SCapabilitiesVisitor { + type Value = SCapabilities; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an array of strings or a map with SCapabilities fields") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut add = CapSet::default(); + while let Some(cap) = seq.next_element::()? { + add.add(cap.parse().map_err(de::Error::custom)?); + } + + Ok(SCapabilities { + default_behavior: SetBehavior::None, + add, + sub: CapSet::default(), + }) + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut default_behavior = SetBehavior::None; + let mut add = CapSet::default(); + let mut sub = CapSet::default(); + let mut _extra_fields = Map::new(); + + while let Some(key) = map.next_key()? { + match key { + Field::Default => { + default_behavior = map + .next_value() + .expect("default entry must be either 'all' or 'none'"); + } + Field::Add => { + let values: Vec = + map.next_value().expect("add entry must be a list"); + for value in values { + add.add(value.parse().map_err(|_| { + de::Error::custom(format!("Invalid capability: {}", value)) + })?); + } + } + Field::Sub => { + let values: Vec = + map.next_value().expect("sub entry must be a list"); + for value in values { + sub.add(value.parse().map_err(|_| { + de::Error::custom(format!("Invalid capability: {}", value)) + })?); + } + } + Field::Other(other) => { + _extra_fields.insert(other.to_string(), map.next_value()?); + } + } + } + + Ok(SCapabilities { + default_behavior, + add, + sub, + }) + } + } + + deserializer.deserialize_any(SCapabilitiesVisitor) + } +} + +impl<'de> Deserialize<'de> for SGenericActorType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let raw: serde_json::Value = Deserialize::deserialize(deserializer)?; + match raw { + serde_json::Value::Number(num) if num.is_u64() => { + Ok(SGenericActorType::Id(num.as_u64().unwrap() as u32)) + } + serde_json::Value::String(ref s) => { + if let Ok(num) = s.parse() { + Ok(SGenericActorType::Id(num)) + } else { + Ok(SGenericActorType::Name(s.clone())) + } + } + _ => Err(serde::de::Error::custom( + "Invalid input for SGenericActorType", + )), + } + } +} + +impl<'de> Deserialize<'de> for SCommands { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize, Display)] + #[serde(field_identifier, rename_all = "kebab-case")] + enum Fields { + #[serde(alias = "d")] + Default, + #[serde(alias = "a")] + Add, + #[serde(alias = "del", alias = "s")] + Sub, + #[serde(untagged)] + Other(String), + } + struct SCommandsVisitor; + impl<'de> Visitor<'de> for SCommandsVisitor { + type Value = SCommands; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string or a number") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut add = Vec::new(); + while let Some(cmd) = seq.next_element::()? { + add.push(cmd); + } + Ok(SCommands { + default_behavior: Some(SetBehavior::None), + add, + sub: Vec::new(), + _extra_fields: Map::new(), + }) + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut default_behavior = None; + let mut add = Vec::new(); + let mut sub = Vec::new(); + let mut _extra_fields = Map::new(); + + while let Some(key) = map.next_key()? { + match key { + Fields::Default => { + default_behavior = Some( + map.next_value() + .expect("default entry must be either 'all' or 'none'"), + ); + } + Fields::Add => { + let values: Vec = + map.next_value().expect("add entry must be a list"); + add.extend(values); + } + Fields::Sub => { + let values: Vec = + map.next_value().expect("sub entry must be a list"); + sub.extend(values); + } + Fields::Other(other) => { + _extra_fields.insert(other.to_string(), map.next_value()?); + } + } + } + + Ok(SCommands { + default_behavior, + add, + sub, + _extra_fields, + }) + } + } + deserializer.deserialize_any(SCommandsVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use capctl::Cap; + use serde_json::json; + + #[test] + fn test_set_behavior_deserialization() { + let json_data = json!("none"); + let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); + assert_eq!(behavior, SetBehavior::None); + + let json_data = json!("all"); + let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); + assert_eq!(behavior, SetBehavior::All); + + let json_data = json!(0); + let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); + assert_eq!(behavior, SetBehavior::from_repr(0).unwrap()); + + let json_data = json!(1); + let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); + assert_eq!(behavior, SetBehavior::from_repr(1).unwrap()); + + let invalid_data = json!(2); + assert!(serde_json::from_value::(invalid_data).is_err()); + } + + #[test] + fn test_s_capabilities_deserialization_seq() { + let json_data = json!(["CAP_SYS_ADMIN", "CAP_NET_BIND_SERVICE", "CAP_CHOWN"]); + let caps: SCapabilities = serde_json::from_value(json_data).unwrap(); + + assert!(caps.add.has(Cap::SYS_ADMIN)); + assert!(caps.add.has(Cap::NET_BIND_SERVICE)); + assert!(caps.add.has(Cap::CHOWN)); + assert_eq!(caps.default_behavior, SetBehavior::None); + } + + #[test] + fn test_s_capabilities_deserialization_map() { + let json_data = json!({ + "default": "none", + "add": ["CAP_SYS_ADMIN", "CAP_CHOWN"], + "sub": ["CAP_NET_RAW"] + }); + + let caps: SCapabilities = serde_json::from_value(json_data).unwrap(); + + assert!(caps.add.has(Cap::SYS_ADMIN)); + assert!(caps.add.has(Cap::CHOWN)); + assert!(caps.sub.has(Cap::NET_RAW)); + assert_eq!(caps.default_behavior, SetBehavior::None); + } + + #[test] + fn test_invalid_capabilities() { + let invalid_data = json!(["INVALID_CAPABILITY", "CAP_FAKE"]); + assert!(serde_json::from_value::(invalid_data).is_err()); + } + + #[test] + fn test_s_generic_actor_type_deserialization() { + let json_data = json!(42); + let actor_type: SGenericActorType = serde_json::from_value(json_data).unwrap(); + assert_eq!(actor_type, SGenericActorType::Id(42)); + + let json_data = json!("actor_name"); + let actor_type: SGenericActorType = serde_json::from_value(json_data).unwrap(); + assert_eq!( + actor_type, + SGenericActorType::Name("actor_name".to_string()) + ); + + let invalid_data = json!(null); + assert!(serde_json::from_value::(invalid_data).is_err()); + } + + #[test] + fn test_s_commands_deserialization_seq() { + let json_data = json!(["/bin/ls", "/bin/cat"]); + + let commands: SCommands = serde_json::from_value(json_data).unwrap(); + + assert_eq!(commands.add.len(), 2); + assert_eq!(commands.add[0], "/bin/ls".into()); + assert_eq!(commands.add[1], "/bin/cat".into()); + } + + #[test] + fn test_s_commands_deserialization_map() { + let json_data = json!({ + "default": "all", + "add": ["/bin/ls"], + "sub": ["/bin/cat"] + }); + + let commands: SCommands = serde_json::from_value(json_data).unwrap(); + + assert_eq!(commands.default_behavior.unwrap(), SetBehavior::All); + assert_eq!(commands.add.len(), 1); + assert_eq!(commands.add[0], "/bin/ls".into()); + assert_eq!(commands.sub.len(), 1); + assert_eq!(commands.sub[0], "/bin/cat".into()); + } +} diff --git a/rar-common/src/database/finder.rs b/rar-common/src/database/finder.rs deleted file mode 100644 index ba0717e2..00000000 --- a/rar-common/src/database/finder.rs +++ /dev/null @@ -1,3393 +0,0 @@ -use std::{ - cell::RefCell, - cmp::Ordering, - error::Error, - fmt::{Display, Formatter}, - path::PathBuf, - rc::{Rc, Weak}, -}; - -use bon::Builder; -use capctl::CapSet; -use glob::Pattern; -use log::{debug, warn}; -use nix::{ - libc::dev_t, - unistd::{Gid, Group, Pid, Uid, User}, -}; -#[cfg(feature = "pcre2")] -use pcre2::bytes::RegexBuilder; - -use strum::EnumIs; - -use crate::database::{ - actor::SActor, - options::{Opt, OptStack}, - structs::{ - SCommand, SCommands, SConfig, SGroupschooser, SRole, STask, SUserChooser, SetBehavior, - }, -}; -use crate::util::{capabilities_are_exploitable, final_path, parse_conf_command}; -use crate::{ - api::{PluginManager, PluginResultAction}, - as_borrow, -}; -use bitflags::bitflags; - -use super::{ - actor::{SGroupType, SGroups, SUserType}, - FilterMatcher, -}; - -#[derive(Debug, PartialEq, Eq, Clone, EnumIs)] -pub enum MatchError { - NoMatch(String), - Conflict(String), -} - -impl Display for MatchError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - MatchError::NoMatch(reason) => write!(f, "No match because : {}", reason), - MatchError::Conflict(reason) => write!(f, "Conflict because : {}", reason), - } - } -} - -impl Error for MatchError { - fn description(&self) -> &str { - match self { - MatchError::NoMatch(_) => "No match", - MatchError::Conflict(_) => "Conflict", - } - } -} - -#[derive(Clone, Debug)] -pub struct ExecSettings { - pub exec_path: PathBuf, - pub exec_args: Vec, - pub opt: OptStack, - pub setuid: Option, - pub setgroups: Option, - pub caps: Option, - pub task: Weak>, -} - -impl ExecSettings { - fn new() -> ExecSettings { - ExecSettings { - exec_path: PathBuf::new(), - exec_args: Vec::new(), - opt: OptStack::default(), - setuid: None, - setgroups: None, - caps: None, - task: Weak::new(), - } - } - - pub fn task(&self) -> Rc> { - self.task.upgrade().expect("Internal Error") - } - - pub fn role(&self) -> Rc> { - self.task() - .as_ref() - .borrow() - .role() - .expect("Internal Error") - } -} - -impl PartialEq for ExecSettings { - fn eq(&self, other: &Self) -> bool { - // We ignore the task field - let res = self.exec_path == other.exec_path - && self.exec_args == other.exec_args - && self.opt == other.opt - && self.setuid == other.setuid - && self.setgroups == other.setgroups - && self.caps == other.caps; - debug!( - "Comparing self.exec_path == other.exec_path : {} - && self.exec_args == other.exec_args : {} - && self.opt == other.opt : {} - && self.setuid == other.setuid : {} - && self.setgroups == other.setgroups : {} - && self.caps == other.caps : {}", - self.exec_path == other.exec_path, - self.exec_args == other.exec_args, - self.opt == other.opt, - self.setuid == other.setuid, - self.setgroups == other.setgroups, - self.caps == other.caps - ); - res - } -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, EnumIs)] -#[repr(u32)] -// Matching user groups for the role -pub enum ActorMatchMin { - UserMatch, - GroupMatch(usize), - NoMatch, -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] - -// Matching setuid and setgid for the role -struct SetuidMin { - is_root: bool, -} -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -struct SetgidMin { - is_root: bool, - nb_groups: usize, -} -impl PartialOrd for SetgidMin { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -impl Ord for SetgidMin { - fn cmp(&self, other: &Self) -> Ordering { - self.is_root - .cmp(&other.is_root) - .then_with(|| self.nb_groups.cmp(&other.nb_groups)) - } -} - -#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)] -pub struct SetUserMin { - uid: Option, - gid: Option, -} -impl PartialOrd for SetUserMin { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -impl Ord for SetUserMin { - fn cmp(&self, other: &Self) -> Ordering { - self.uid - .cmp(&other.uid) - .then_with(|| self.gid.cmp(&other.gid)) - } -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] -pub struct CmdMin(u32); - -bitflags! { - - impl CmdMin: u32 { - const Match = 0b00001; - const WildcardPath = 0b00010; - const RegexArgs = 0b00100; - const FullRegexArgs = 0b01000; - const FullWildcardPath = 0b10000; - } -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] -pub enum CapsMin { - Undefined, - NoCaps, - CapsNoAdmin(usize), - CapsAdmin(usize), - CapsAll, -} - -#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] -pub struct SecurityMin(u32); - -bitflags! { - - impl SecurityMin: u32 { - const DisableBounding = 0b000001; - const EnableRoot = 0b000010; - const KeepEnv = 0b000100; - const KeepPath = 0b001000; - const KeepUnsafePath = 0b010000; - const SkipAuth = 0b100000; - } -} - -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub struct Score { - pub user_min: ActorMatchMin, - pub cmd_min: CmdMin, - pub caps_min: CapsMin, - pub setuser_min: SetUserMin, - pub security_min: SecurityMin, -} - -impl Score { - pub fn prettyprint(&self) -> String { - format!( - "{:?}, {:?}, {:?}, {:?}, {:?}", - self.user_min, self.cmd_min, self.caps_min, self.setuser_min, self.security_min - ) - } - - pub fn user_cmp(&self, other: &Score) -> Ordering { - self.user_min.cmp(&other.user_min) - } - - /// Compare the score of tasks results - pub fn cmd_cmp(&self, other: &Score) -> Ordering { - self.cmd_min - .cmp(&other.cmd_min) - .then(self.caps_min.cmp(&other.caps_min)) - .then(self.setuser_min.cmp(&other.setuser_min)) - .then(self.security_min.cmp(&other.security_min)) - } -} - -impl PartialOrd for Score { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Score { - fn cmp(&self, other: &Self) -> Ordering { - self.cmd_cmp(other).then(self.user_cmp(other)) - } - - fn max(self, other: Self) -> Self { - std::cmp::max_by(self, other, Ord::cmp) - } - - fn min(self, other: Self) -> Self { - std::cmp::min_by(self, other, Ord::cmp) - } - - fn clamp(self, min: Self, max: Self) -> Self { - self.max(min).min(max) - } -} - -#[derive(Debug, Builder)] -pub struct Cred { - #[builder(field)] - pub groups: Vec, - #[builder(field = User::from_uid(Uid::current()).unwrap().unwrap())] - pub user: User, - pub tty: Option, - #[builder(default = nix::unistd::getppid(), into)] - pub ppid: Pid, -} - -impl CredBuilder { - pub fn user_id(mut self, uid: impl Into) -> Self { - self.user = User::from_uid(uid.into()).unwrap().unwrap(); - self - } - pub fn user_name(mut self, name: impl Into) -> Self { - self.user = User::from_name(&name.into()).unwrap().unwrap(); - self - } - pub fn group_id(mut self, gid: impl Into) -> Self { - self.groups - .push(Group::from_gid(gid.into()).unwrap().unwrap()); - self - } - pub fn group_name(mut self, name: impl Into) -> Self { - self.groups - .push(Group::from_name(&name.into()).unwrap().unwrap()); - self - } - pub fn groups(mut self, groups: Vec) -> Self { - self.groups = groups - .iter() - .map(|gid| Group::from_gid(*gid).unwrap().unwrap()) - .collect(); - self - } -} - -#[derive(Clone, Debug)] -pub struct TaskMatch { - pub score: Score, - pub settings: ExecSettings, -} - -impl TaskMatch { - pub fn fully_matching(&self) -> bool { - self.user_matching() && self.command_matching() - } - - pub fn user_matching(&self) -> bool { - self.score.user_min != ActorMatchMin::NoMatch - } - - pub fn command_matching(&self) -> bool { - !self.score.cmd_min.is_empty() - } - - pub fn task(&self) -> Rc> { - self.settings.task.upgrade().expect("Internal Error") - } - - pub fn role(&self) -> Rc> { - self.task() - .as_ref() - .borrow() - .role() - .expect("Internal Error") - } -} - -impl Default for TaskMatch { - fn default() -> Self { - TaskMatch { - score: Score { - user_min: ActorMatchMin::NoMatch, - cmd_min: CmdMin::empty(), - caps_min: CapsMin::Undefined, - setuser_min: SetUserMin::default(), - security_min: SecurityMin::empty(), - }, - settings: ExecSettings::new(), - } - } -} - -pub trait TaskMatcher { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result; -} - -pub trait CredMatcher { - fn user_matches(&self, user: &Cred) -> ActorMatchMin; -} - -fn find_from_envpath(needle: &PathBuf) -> Option { - if needle.is_absolute() { - return None; - } - let env_path = std::env::var_os("PATH").unwrap(); - for path in std::env::split_paths(&env_path) { - let path = path.join(needle); - if path.exists() { - return Some(path); - } - } - None -} - -fn match_path(input_path: &str, role_path: &String) -> CmdMin { - if role_path == "**" { - return CmdMin::FullWildcardPath; - } - let mut match_status = CmdMin::empty(); - let new_path = final_path(input_path); - let role_path = final_path(role_path); - debug!("Matching path {:?} with {:?}", new_path, role_path); - if new_path == role_path { - match_status |= CmdMin::Match; - } else if let Ok(pattern) = Pattern::new(role_path.to_str().unwrap()) { - if pattern.matches_path(&new_path) { - match_status |= CmdMin::WildcardPath; - } - } - if match_status.is_empty() { - debug!( - "No match for path ``{:?}`` for evaluated path : ``{:?}``", - new_path, role_path - ); - } - match_status -} - -/// Check if input args is matching with role args and return the score -/// role args can contains regex -/// input args is the command line args -fn match_args(input_args: &[String], role_args: &[String]) -> Result> { - if role_args[0] == ".*" { - return Ok(CmdMin::FullRegexArgs); - } - let commandline = input_args.join(" "); - let role_args = role_args.join(" "); - debug!("Matching args {:?} with {:?}", commandline, role_args); - if commandline != role_args { - debug!("test regex"); - evaluate_regex_cmd(role_args, commandline).inspect_err(|e| { - debug!("{:?},No match for args {:?}", e, input_args); - }) - } else { - Ok(CmdMin::Match) - } -} - -#[cfg(feature = "pcre2")] -fn evaluate_regex_cmd(role_args: String, commandline: String) -> Result> { - let regex = RegexBuilder::new().build(&role_args)?; - if regex.is_match(commandline.as_bytes())? { - Ok(CmdMin::RegexArgs) - } else { - Err(Box::new(MatchError::NoMatch( - "Regex for command does not match".to_string(), - ))) - } -} - -#[cfg(not(feature = "pcre2"))] -fn evaluate_regex_cmd(_role_args: String, _commandline: String) -> Result> { - Err(Box::new(MatchError::NoMatch)) -} - -/// Check if input command line is matching with role command line and return the score -fn match_command_line(input_command: &[String], role_command: &[String]) -> CmdMin { - let mut result = CmdMin::empty(); - if !input_command.is_empty() { - result = match_path(&input_command[0], &role_command[0]); - if result.is_empty() || role_command.len() == 1 { - return result; - } - match match_args(&input_command[1..], &role_command[1..]) { - Ok(args_result) => result |= args_result, - Err(err) => { - if err.downcast_ref::().is_none() { - warn!("Error: {}", err); - } - return CmdMin::empty(); - } - } - } - result -} - -/// Find the minimum score for all commands that match the input command line -fn get_cmd_min(input_command: &[String], commands: &[SCommand]) -> CmdMin { - let mut min_score: CmdMin = CmdMin::empty(); - debug!("Input {:?} matches with {:?}", input_command, commands); - for command in commands { - match parse_conf_command(command) { - Ok(command) => { - let new_score = match_command_line(input_command, &command); - debug!("Score for command {:?} is {:?}", command, new_score); - if !new_score.is_empty() && (min_score.is_empty() || (new_score < min_score)) { - debug!("New min score for command {:?} is {:?}", command, new_score); - min_score = new_score; - } - } - Err(err) => { - warn!("Error: {}", err); - } - } - } - min_score -} - -fn get_caps_min(caps: &Option) -> CapsMin { - match caps { - Some(caps) => { - if caps.is_empty() { - CapsMin::NoCaps - } else if *caps == !CapSet::empty() { - CapsMin::CapsAll - } else if capabilities_are_exploitable(caps) { - CapsMin::CapsAdmin(caps.size()) - } else { - CapsMin::CapsNoAdmin(caps.size()) - } - } - None => CapsMin::NoCaps, - } -} - -fn get_security_min(opt: &Option>>) -> SecurityMin { - match opt { - Some(opt) => { - let opt = opt.as_ref().borrow(); - let mut result = SecurityMin::empty(); - if let Some(value) = opt.bounding { - if value.is_strict() { - result |= SecurityMin::DisableBounding; - } - } - if let Some(value) = opt.root { - if value.is_privileged() { - result |= SecurityMin::EnableRoot; - } - } - if let Some(value) = &opt.path { - if value.default_behavior.is_keep_unsafe() { - result |= SecurityMin::KeepUnsafePath; - } else if value.default_behavior.is_keep_safe() { - result |= SecurityMin::KeepPath; - } - } - if let Some(value) = &opt.env { - if value.default_behavior.is_keep() { - result |= SecurityMin::KeepEnv; - } - } - if opt.authentication.is_some_and(|auth| auth.is_skip()) { - result |= SecurityMin::SkipAuth; - } - result - } - None => SecurityMin::empty(), - } -} - -fn group_is_root(actortype: &SGroupType) -> bool { - (*actortype).fetch_id().map_or(false, |id| id == 0) -} - -fn user_is_root(actortype: &SUserType) -> bool { - (*actortype).fetch_id().map_or(false, |id| id == 0) -} - -fn groups_contains_root(list: Option<&SGroups>) -> bool { - if let Some(list) = list { - match list { - SGroups::Single(group) => group_is_root(group), - SGroups::Multiple(groups) => groups.iter().any(group_is_root), - } - } else { - false - } -} - -fn groups_len(groups: Option<&SGroups>) -> usize { - match groups { - Some(groups) => groups.len(), - None => 0, - } -} - -fn get_setuid_min( - setuid: Option<&SUserType>, - setgid: Option<&SGroups>, - security_min: &SecurityMin, -) -> SetUserMin { - match (setuid, setgid) { - (Some(setuid), setgid) => { - if security_min.contains(SecurityMin::EnableRoot) { - // root is privileged - if user_is_root(setuid) { - if groups_contains_root(setgid) { - SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: true, - nb_groups: (groups_len(setgid)), - }), - } - } else if setgid.is_none() || groups_len(setgid) == 0 { - SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: None, - } - } else { - SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: false, - nb_groups: (groups_len(setgid)), - }), - } - } - } else if groups_contains_root(setgid) { - SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: Some(SetgidMin { - is_root: true, - nb_groups: (groups_len(setgid)), - }), - } - } else if setgid.is_none() || groups_len(setgid) == 0 { - SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: None, - } - } else { - SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: Some(SetgidMin { - is_root: false, - nb_groups: (groups_len(setgid)), - }), - } - } - } else { - // root is a user - SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: Some(SetgidMin { - is_root: false, - nb_groups: (groups_len(setgid)), - }), - } - } - } - (None, setgid) => { - let len = groups_len(setgid); - if len == 0 { - SetUserMin { - uid: None, - gid: None, - } - } else if security_min.contains(SecurityMin::EnableRoot) && groups_contains_root(setgid) - { - SetUserMin { - uid: None, - gid: Some(SetgidMin { - is_root: true, - nb_groups: len, - }), - } - } else { - SetUserMin { - uid: None, - gid: Some(SetgidMin { - is_root: false, - nb_groups: len, - }), - } - } - } - } -} - -impl TaskMatcher for Rc> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - if let Some(cmd_opt) = cmd_opt { - if let Some(task) = &cmd_opt.task { - if task != &self.as_ref().borrow().name.to_string() { - debug!("Task {} does not match", self.as_ref().borrow().name); - return Err(MatchError::NoMatch("Task name does not match".to_string())); - } - } - } - debug!("Matching task {}", self.as_ref().borrow().name); - - // Match initial task commands - let TaskMatch { - mut score, - mut settings, - } = self - .as_ref() - .borrow() - .commands - .matches(user, cmd_opt, command)?; - - // Process capabilities and security - let capset = self - .as_ref() - .borrow() - .cred - .capabilities - .as_ref() - .map(|caps| caps.to_capset()); - score.caps_min = get_caps_min(&capset); - score.security_min = get_security_min(&self.as_ref().borrow().options); - if cmd_opt - .as_ref() - .and_then(|filter| filter.env_behavior) // if the command wants to override the behavior - .as_ref() - .is_some_and(|behavior| { - settings - .opt - .to_opt() // at this point we own the opt structure - .as_ref() - .borrow() - .env - .as_ref() - .is_some_and(|env| { - !env.override_behavior.is_some_and(|b| b) - || env.default_behavior == *behavior - }) - // but the polcy deny it and the behavior is not the same as the default one - // we return NoMatch - // (explaination: if the behavior is the same as the default one, we don't override it) - }) - { - return Err(MatchError::NoMatch( - "The user wants to override the behavior but the policy deny it".to_string(), - )); - } - // Processing setuid - let setuid: Option = self.as_ref().borrow().cred.setuid.clone(); - let setuid_result = match setuid { - Some(SUserChooser::Actor(s)) => Some(s), - Some(SUserChooser::ChooserStruct(t)) => { - match cmd_opt.as_ref().and_then(|cmd| cmd.user.as_ref()) { - None => { - debug!( - "Aucun utilisateur spécifié dans la commande, fallback utilisé : {:?}", - t.fallback - ); - Some(t.fallback.clone()) // Retourne le fallback si aucun utilisateur n'est spécifié - } - Some(user) => { - debug!("Utilisateur spécifié dans la commande : {}", user); - - // Comparer l'utilisateur spécifié avec le fallback - if user.fetch_eq(&t.fallback) { - debug!( - "L'utilisateur spécifié dans la commande correspond au fallback !" - ); - Some(t.fallback.clone()) // Si l'utilisateur correspond au fallback, utiliser le fallback - } else if t.sub.iter().any(|s| s.fetch_eq(user)) { - // Si l'utilisateur est explicitement interdit dans `sub` - return Err(MatchError::NoMatch( - "L'utilisateur est interdit dans sub.".into(), - )); - } else if t.add.iter().any(|s| s.fetch_eq(user)) { - // Si l'utilisateur est explicitement autorisé dans `add` - - Some(user.clone()) // Retourner une erreur immédiate - } else { - // Aucun match explicite, appliquer le comportement par défaut - match t.default { - SetBehavior::None => { - return Err(MatchError::NoMatch( - "Aucun comportement par défaut applicable.".into(), - )); // Aucun utilisateur par défaut - } - SetBehavior::All => { - debug!("Tous les utilisateurs sont acceptés."); - Some(user.clone()) // Tout utilisateur accepté - } - } - } - } - } - } - None => None, - }; - - // Set gid processing - let setgid = &self.as_ref().borrow().cred.setgid; - let setgid_result: Option = match setgid { - Some(SGroupschooser::Group(s)) => Some(s.clone()), - Some(SGroupschooser::StructChooser(m)) => { - match cmd_opt.as_ref().and_then(|cmd| cmd.group.as_ref()) { - None => { - debug!( - "No group specified in the command, fallback used : {:?}", - m.fallback - ); - Some(m.fallback.clone()) - } - Some(ggroup) => { - debug!("Group specified in the command : {:?}", ggroup); - if ggroup.fetch_eq(&m.fallback) { - debug!("The group specified in the command matches the fallback !"); - Some(m.fallback.clone()) - } else if m.sub.iter().any(|s| s.fetch_eq(ggroup)) { - return Err(MatchError::NoMatch( - "The group is forbidden in sub.".into(), - )); - } else if m.add.iter().any(|s| s.fetch_eq(ggroup)) { - Some(ggroup.clone()) - } else { - match m.default { - SetBehavior::None => { - return Err(MatchError::NoMatch( - "No default behavior applicable.".into(), - )); - } - SetBehavior::All => { - debug!("All groups are accepted."); - Some(ggroup.clone()) - } - } - } - } - } - } - None => None, - }; - - // Calculate setuid and setgid minimum - score.setuser_min = get_setuid_min( - setuid_result.as_ref(), - setgid_result.as_ref(), - &score.security_min, - ); - - // Update task settings - settings.setuid = setuid_result.clone(); - settings.setgroups = setgid_result.clone(); - settings.caps = capset; - - // Get options stack from the task - let stack = OptStack::from_task(self.clone()); - settings.opt = stack; - - // Return the final TaskMatch - Ok(TaskMatch { score, settings }) - } -} - -fn get_default_behavior(commands: &Option) -> &SetBehavior { - match commands.as_ref() { - Some(commands) => commands, - None => &SetBehavior::None, - } -} - -impl TaskMatcher for SCommands { - fn matches( - &self, - _: &Cred, - _: &Option, - input_command: &[String], - ) -> Result { - let min_score: CmdMin; - let mut settings = ExecSettings::new(); - // if the command is forbidden, we return NoMatch - debug!("Checking if command is forbidden"); - let is_forbidden = get_cmd_min(input_command, &self.sub); - if !is_forbidden.is_empty() { - debug!("Command is forbidden"); - return Err(MatchError::NoMatch("Command is forbidden".to_string())); - } - // otherwise, we check if behavior is No command allowed by default - if get_default_behavior(&self.default_behavior).is_none() { - debug!("Checking if command is allowed by default"); - // if the behavior is No command by default, we check if the command is allowed explicitly. - min_score = get_cmd_min(input_command, &self.add); - if min_score.is_empty() { - return Err(MatchError::NoMatch("Command is not allowed".to_string())); - } - } else { - min_score = CmdMin::all(); - debug!("Command is allowed by default"); - } - - if let Some(program) = - find_from_envpath(&input_command[0].parse().expect("The path is not valid")) - { - settings.exec_path = program; - settings.exec_args = input_command[1..].to_vec(); - } else { - // encapsulate the command in sh command - settings.exec_path = PathBuf::from("/bin/sh"); - settings.exec_args = vec!["-c".to_string(), shell_words::join(input_command)]; - } - - Ok(TaskMatch { - score: Score { - user_min: ActorMatchMin::NoMatch, - cmd_min: min_score, - caps_min: CapsMin::Undefined, - setuser_min: SetUserMin::default(), - security_min: SecurityMin::empty(), - }, - settings, - }) - } -} - -/// Check if user's groups is matching with any of the role's groups -fn match_groups(groups: &[Group], role_groups: &[SGroups]) -> bool { - for role_group in role_groups { - if match role_group { - SGroups::Single(group) => { - debug!( - "Checking group {}, with {:?}, it must be {}", - group, - groups, - groups.iter().any(|g| group == g) - ); - groups.iter().any(|g| group == g) - } - SGroups::Multiple(multiple_actors) => multiple_actors.iter().all(|actor| { - debug!("Checking group {}, with {:?}", actor, groups); - groups.iter().any(|g| actor == g) - }), - } { - return true; - } - } - false -} - -impl CredMatcher for Rc> { - fn user_matches(&self, user: &Cred) -> ActorMatchMin { - let borrow = self.as_ref().borrow(); - if PluginManager::notify_duty_separation(&self.as_ref().borrow(), user).is_deny() { - warn!("You are forbidden to use a role due to a conflict of interest, please contact your administrator"); - return ActorMatchMin::NoMatch; - } - let matches = borrow.actors.iter().filter_map(|actor| { - match actor { - SActor::User { id, .. } => { - if let Some(id) = id { - if *id == user.user { - return Some(ActorMatchMin::UserMatch); - } - } - } - SActor::Group { groups, .. } => { - if let Some(groups) = groups.as_ref() { - if match_groups(&user.groups, &[groups.clone()]) { - return Some(ActorMatchMin::GroupMatch(groups.len())); - } - } - } - SActor::Unknown(element) => { - let min = PluginManager::notify_user_matcher(&as_borrow!(self), user, element); - if !min.is_no_match() { - return Some(min); - } - } - } - None - }); - let min = matches.min().unwrap_or(ActorMatchMin::NoMatch); - debug!( - "Role {} : User {} matches with {:?}", - borrow.name, user.user.name, min - ); - min - } -} - -impl TaskMatcher for Vec>> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - let mut min_task = TaskMatch::default(); - let mut nmatch = 0; - for task in self.iter() { - match task.matches(user, cmd_opt, command) { - Ok(mut task_match) => { - if !min_task.command_matching() - || task_match.score.cmd_cmp(&min_task.score) == Ordering::Less - { - task_match.score.user_min = min_task.score.user_min; - task_match.settings.task = Rc::downgrade(task); - min_task = task_match; - nmatch = 1; - } else if task_match.score == min_task.score - && task_match.settings != min_task.settings - { - nmatch += 1; - } - } - Err(err) => match err { - MatchError::NoMatch(_) => { - continue; - } - MatchError::Conflict(_) => { - return Err(err); - } - }, - } - } - debug!("nmatch = {}", nmatch); - if nmatch == 0 { - Err(MatchError::NoMatch("No tasks matched".into())) - } else if nmatch == 1 { - Ok(min_task) - } else { - Err(MatchError::Conflict("Multiple tasks matched".into())) - } - } -} - -impl TaskMatcher for Vec>> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - let mut min_role = TaskMatch::default(); - let mut nmatch = 0; - for role in self.iter() { - match role.matches(user, cmd_opt, command) { - Ok(mut role_match) => { - role_match.score.user_min = min_role.score.user_min; - if min_role.score.cmd_min.is_empty() || role_match.score < min_role.score { - min_role = role_match; - nmatch = 1; - } else if role_match.score == min_role.score - && !Rc::ptr_eq( - &role_match.settings.task.upgrade().unwrap(), - &min_role.settings.task.upgrade().unwrap(), - ) - { - nmatch += 1; - } - } - Err(err) => { - if err.is_no_match() { - continue; - } else { - return Err(err); - } - } - } - } - if nmatch == 0 { - Err(MatchError::NoMatch("No roles matched".into())) - } else if nmatch == 1 { - Ok(min_role) - } else { - Err(MatchError::Conflict("Multiple roles matched".into())) - } - } -} - -impl TaskMatcher for Rc> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - if let Some(cmd_opt) = cmd_opt { - if let Some(role) = &cmd_opt.role { - if role != &self.as_ref().borrow().name { - return Err(MatchError::NoMatch("Role name does not match".to_string())); - } - } - } - let borrow = self.as_ref().borrow(); - let mut min_role = TaskMatch::default(); - let user_min = self.user_matches(user); - min_role.score.user_min = user_min; - - let mut nmatch = 0; - - match borrow.tasks.matches(user, cmd_opt, command) { - Ok(task_match) => { - if !min_role.fully_matching() - || (task_match.command_matching() && task_match.score < min_role.score) - { - min_role = task_match; - nmatch = 1; - } - } - Err(MatchError::NoMatch(_)) => { - nmatch = 0; - } - Err(MatchError::Conflict(msg)) => { - return Err(MatchError::Conflict(msg)); - } - } - min_role.score.user_min = user_min; - plugin_role_match( - user_min, - borrow, - user, - cmd_opt, - command, - &mut min_role, - &mut nmatch, - ); - debug!( - "==== Role {} ====\n score: {}", - self.as_ref().borrow().name, - min_role.score.prettyprint() - ); - if nmatch == 0 { - Err(MatchError::NoMatch("No tasks matched".into())) - } else if nmatch == 1 { - debug!( - "=== Role {} === : Match for task {}\nScore : {}", - self.as_ref().borrow().name, - min_role.task().as_ref().borrow().name.to_string(), - min_role.score.prettyprint() - ); - Ok(min_role) - } else { - Err(MatchError::Conflict("Multiple tasks matched".into())) - } - } -} - -fn plugin_role_match( - user_min: ActorMatchMin, - borrow: std::cell::Ref<'_, SRole>, - user: &Cred, - cmd_opt: &Option, - command: &[String], - min_role: &mut TaskMatch, - nmatch: &mut i32, -) { - let mut matcher = TaskMatch::default(); - matcher.score.user_min = user_min; - // notify plugins - match PluginManager::notify_role_matcher(&borrow, user, cmd_opt, command, &mut matcher) { - PluginResultAction::Override => { - *min_role = matcher; - *nmatch = if min_role.fully_matching() { 1 } else { 0 }; - } - PluginResultAction::Edit => { - debug!("Plugin edit"); - if !min_role.command_matching() - || (matcher.command_matching() && matcher.score.cmd_min < min_role.score.cmd_min) - { - *min_role = matcher; - *nmatch = 1; - } else if matcher.score == min_role.score { - *nmatch += 1; - } else if !matcher.fully_matching() { - *nmatch = 0; - } - } - PluginResultAction::Ignore => {} - } - debug!("nmatch = {}", nmatch); -} - -impl TaskMatcher for Rc> { - fn matches( - &self, - user: &Cred, - cmd_opt: &Option, - command: &[String], - ) -> Result { - debug!( - "Config : Matching user {} with command {:?}", - user.user.name, command - ); - let mut tasks: Vec = Vec::new(); - for role in self.as_ref().borrow().roles.iter() { - if let Ok(matched) = role.matches(user, cmd_opt, command) { - if matched.fully_matching() { - if tasks.is_empty() || matched.score < tasks[0].score { - tasks.clear(); - tasks.push(matched); - } else if matched.score == tasks[0].score - && !Rc::ptr_eq( - &matched.settings.task.upgrade().unwrap(), - &tasks[0].settings.task.upgrade().unwrap(), - ) - { - tasks.push(matched); - } - } - } // we ignore error, because it's not a match - } - if tasks.is_empty() { - Err(MatchError::NoMatch("No roles matched".into())) - } else if tasks.len() > 1 { - Err(MatchError::Conflict("Multiple roles matched".into())) - } else { - debug!( - "Config : Matched user {}\n - command {:?}\n - with task {}\n - with role {}\n - with score {:?}", - user.user.name, - command, - tasks[0].task().as_ref().borrow().name.to_string(), - tasks[0].role().as_ref().borrow().name, - tasks[0].score.prettyprint() - ); - Ok(tasks[0].clone()) - } - } -} - -#[cfg(test)] -mod tests { - - use std::{fs, vec}; - - use capctl::Cap; - use test_log::test; - - use crate::{ - database::{ - make_weak_config, - options::{EnvBehavior, PathBehavior, SAuthentication, SBounding, SPrivileged}, - structs::{IdTask, RoleGetter, SCredentials, SSetgidSet, SSetuidSet}, - versionning::Versioning, - }, - rc_refcell, - }; - - use super::*; - - fn get_non_root_uid() -> u32 { - // list all users - let passwd = fs::read_to_string("/etc/passwd").unwrap(); - let passwd: Vec<&str> = passwd.split('\n').collect(); - return passwd - .iter() - .map(|line| { - let line: Vec<&str> = line.split(':').collect(); - line[2].parse::().unwrap() - }) - .find(|uid| *uid != 0) - .unwrap(); - } - - fn get_non_root_gid(nth: usize) -> Option { - // list all users - let passwd = fs::read_to_string("/etc/group").unwrap(); - let passwd: Vec<&str> = passwd.split('\n').collect(); - return passwd - .iter() - .map(|line| { - let line: Vec<&str> = line.split(':').collect(); - line[2].parse::().unwrap() - }) - .filter(|uid| *uid != 0) - .nth(nth); - } - - #[test] - fn test_find_from_envpath() { - let needle = PathBuf::from("ls"); - let result = find_from_envpath(&needle); - println!("{:?}", result); - assert_eq!(result, Some("/usr/bin/ls".into())); - } - - #[test] - fn test_find_from_envpath_absolute_path() { - // Avec un chemin absolu - let needle = PathBuf::from("/bin/ls"); - let result = find_from_envpath(&needle); - println!("{:?}", result); - assert_eq!(result, None); - } - - #[test] - fn test_find_from_envpath_not_found() { - // Avec un fichier qui n'existe pas dans le PATH. - let needle = PathBuf::from("no_path"); - let result = find_from_envpath(&needle); - println!("{:?}", result); - assert_eq!(result, None); - } - - #[test] - fn test_match_path() { - let result = match_path(&"/bin/ls".to_string(), &"/bin/ls".to_string()); - assert_eq!(result, CmdMin::Match); - } - - #[test] - fn test_match_args() { - let result = match_args( - &["-l".to_string(), "-a".to_string()], - &["-l".to_string(), "-a".to_string()], - ); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), CmdMin::Match); - } - - #[test] - fn test_match_command_line() { - let result = match_command_line( - &["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()], - &["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()], - ); - assert_eq!(result, CmdMin::Match); - } - - #[test] - fn test_get_cmd_min() { - let result = get_cmd_min( - &["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()], - &[ - "/bin/l*".into(), - "/bin/ls .*".into(), - "/bin/ls -l -a".into(), - ], - ); - assert_eq!(result, CmdMin::Match); - } - - #[test] - fn test_get_caps_min_all() { - let caps = !CapSet::empty(); - assert_eq!(get_caps_min(&Some(caps)), CapsMin::CapsAll); - } - - #[test] - fn test_get_caps_min_no_admin() { - let mut caps = CapSet::empty(); - caps.add(Cap::NET_BIND_SERVICE); - assert_eq!(get_caps_min(&Some(caps)), CapsMin::CapsNoAdmin(1)); - } - - #[test] - fn test_get_caps_min_admin() { - let mut caps = CapSet::empty(); - caps.add(Cap::SYS_ADMIN); - assert_eq!(get_caps_min(&Some(caps)), CapsMin::CapsAdmin(1)); - } - - #[test] - fn test_get_caps_min_no_caps() { - assert_eq!(get_caps_min(&None), CapsMin::NoCaps); - } - - #[test] - fn test_get_security_min() { - let rcopt = Rc::new(RefCell::new(Opt::default())); - { - let opt = &mut rcopt.as_ref().borrow_mut(); - opt.bounding = Some(SBounding::Strict); - opt.root = Some(SPrivileged::Privileged); - opt.path.as_mut().unwrap().default_behavior = PathBehavior::KeepUnsafe; - opt.env.as_mut().unwrap().default_behavior = EnvBehavior::Keep; - opt.authentication = Some(SAuthentication::Skip); - } - - assert_eq!( - get_security_min(&Some(rcopt.clone())), - SecurityMin::DisableBounding - | SecurityMin::EnableRoot - | SecurityMin::KeepUnsafePath - | SecurityMin::KeepEnv - | SecurityMin::SkipAuth - ); - rcopt - .as_ref() - .borrow_mut() - .path - .as_mut() - .unwrap() - .default_behavior = PathBehavior::KeepSafe; - assert_eq!( - get_security_min(&Some(rcopt.clone())), - SecurityMin::DisableBounding - | SecurityMin::EnableRoot - | SecurityMin::KeepPath - | SecurityMin::KeepEnv - | SecurityMin::SkipAuth - ); - } - - #[test] - fn test_is_root() { - assert!(user_is_root(&"root".into())); - assert!(user_is_root(&0.into())); - assert!(!user_is_root(&1.into())); - assert!(group_is_root(&"root".into())); - assert!(group_is_root(&0.into())); - assert!(!group_is_root(&1.into())); - } - - #[test] - fn test_list_contains_root() { - let mut list = SGroups::Single("root".into()); - assert!(groups_contains_root(Some(&list))); - list = SGroups::Multiple(vec!["root".into(), 1.into()]); - assert!(groups_contains_root(Some(&list))); - list = SGroups::Multiple(vec![1.into(), 2.into()]); - assert!(!groups_contains_root(Some(&list))); - } - - #[test] - fn test_get_setuid_min() { - let mut setuid: Option = Some("root".into()); - let mut setgid = Some(SGroups::Single("root".into())); - let security_min = SecurityMin::EnableRoot; - assert_eq!( - get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), - SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: true, - nb_groups: 1 - }) - } - ); - setuid = Some("1".into()); - assert_eq!( - get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), - SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: Some(SetgidMin { - is_root: true, - nb_groups: 1 - }) - } - ); - setgid = Some(SGroups::Multiple(vec![1.into(), 2.into()])); - assert_eq!( - get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), - SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: Some(SetgidMin { - is_root: false, - nb_groups: 2 - }) - } - ); - assert_eq!( - get_setuid_min(None, setgid.as_ref(), &security_min), - SetUserMin { - uid: None, - gid: Some(SetgidMin { - is_root: false, - nb_groups: 2 - }) - } - ); - assert_eq!( - get_setuid_min(None, None, &security_min), - SetUserMin { - uid: None, - gid: None - } - ); - assert_eq!( - get_setuid_min(setuid.as_ref(), None, &security_min), - SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: None - } - ); - let setuid: Option = Some("root".into()); - assert_eq!( - get_setuid_min(setuid.as_ref(), None, &security_min), - SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: None, - } - ); - setgid = Some(SGroups::Multiple(vec![1.into(), 2.into()])); - assert_eq!( - get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), - SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: false, - nb_groups: 2, - }), - } - ); - setgid = Some(SGroups::Multiple(vec![])); - assert_eq!( - get_setuid_min(setuid.as_ref(), setgid.as_ref(), &security_min), - SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: None, - } - ); - - setgid = Some(SGroups::Multiple(vec![0.into()])); - assert_eq!( - get_setuid_min(None, setgid.as_ref(), &security_min), - SetUserMin { - uid: None, - gid: Some(SetgidMin { - is_root: true, - nb_groups: 1, - }), - } - ); - } - - #[test] - fn test_score_cmp() { - let score1 = Score { - user_min: ActorMatchMin::UserMatch, - cmd_min: CmdMin::Match, - caps_min: CapsMin::CapsAll, - setuser_min: SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: true, - nb_groups: 1, - }), - }, - security_min: SecurityMin::DisableBounding | SecurityMin::EnableRoot, - }; - let mut score2 = Score { - user_min: ActorMatchMin::UserMatch, - cmd_min: CmdMin::Match, - caps_min: CapsMin::CapsAll, - setuser_min: SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: true, - nb_groups: 1, - }), - }, - security_min: SecurityMin::DisableBounding, - }; - assert_eq!(score1.cmp(&score2), Ordering::Greater); - assert_eq!(score2.cmp(&score1), Ordering::Less); - assert_eq!(score1.max(score2), score1); - assert_eq!(score1.min(score2), score2); - assert_eq!(score1.clamp(score2, score1), score1); - assert_eq!(score1.clamp(score2, score2), score2); - score2.security_min = SecurityMin::DisableBounding | SecurityMin::EnableRoot; - assert_eq!(score1.cmp(&score2), Ordering::Equal); - score2.setuser_min = SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: true, - nb_groups: 2, - }), - }; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.setuser_min = SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: Some(SetgidMin { - is_root: true, - nb_groups: 2, - }), - }; - assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.setuser_min = SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: false, - nb_groups: 2, - }), - }; - assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.setuser_min = SetUserMin { - uid: Some(SetuidMin { is_root: false }), - gid: Some(SetgidMin { - is_root: false, - nb_groups: 2, - }), - }; - assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.setuser_min = SetUserMin { - uid: Some(SetuidMin { is_root: true }), - gid: Some(SetgidMin { - is_root: true, - nb_groups: 1, - }), - }; - assert_eq!(score1.cmp(&score2), Ordering::Equal); - score2.caps_min = CapsMin::CapsAdmin(1); - assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.caps_min = CapsMin::CapsNoAdmin(1); - assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.caps_min = CapsMin::NoCaps; - assert_eq!(score1.cmp(&score2), Ordering::Greater); - score2.caps_min = CapsMin::CapsAll; - assert_eq!(score1.cmp(&score2), Ordering::Equal); - score2.cmd_min = CmdMin::FullWildcardPath; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.cmd_min = CmdMin::WildcardPath; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.cmd_min = CmdMin::RegexArgs; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.cmd_min = CmdMin::FullRegexArgs; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.cmd_min = CmdMin::Match; - assert_eq!(score1.cmp(&score2), Ordering::Equal); - score2.user_min = ActorMatchMin::GroupMatch(1); - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.user_min = ActorMatchMin::NoMatch; - assert_eq!(score1.cmp(&score2), Ordering::Less); - score2.user_min = ActorMatchMin::UserMatch; - assert_eq!(score1.cmp(&score2), Ordering::Equal); - } - - fn setup_test_config(num_roles: usize) -> Rc> { - SConfig::builder() - .roles((0..num_roles).map(|i| SRole::builder(format!("role{}", i)).build())) - .build() - } - - fn setup_test_role( - num_tasks: usize, - role: Option>>, - with_config: Option>>, - ) -> Rc> { - let role = role.unwrap_or_else(|| { - let mut role = SRole::default(); - role.name = "test".to_string(); - role._config = with_config.map(|config| Rc::downgrade(&config)); - Rc::new(RefCell::new(role)) - }); - for i in 0..num_tasks { - let mut task = STask::default(); - task.name = IdTask::Name(format!("{}_task_{}", role.as_ref().borrow().name, i)); - task._role = Some(Rc::downgrade(&role)); - role.as_ref().borrow_mut().tasks.push(Rc::new(task.into())); - } - role - } - - #[test] - fn test_matcher_matches() { - let config = setup_test_config(2); - let role0 = setup_test_role(2, Some(config.as_ref().borrow().roles[0].clone()), None); - let r0_task0 = role0.as_ref().borrow().tasks[0].clone(); - let r0_task1 = role0.as_ref().borrow().tasks[1].clone(); - let role1 = setup_test_role(2, Some(config.as_ref().borrow().roles[1].clone()), None); - let r1_task0 = role1.as_ref().borrow().tasks[0].clone(); - let r1_task1 = role1.as_ref().borrow().tasks[1].clone(); - - // every tasks matches but not at the same score, so the least one is matched - role0 - .as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - role1 - .as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - r0_task0 - .as_ref() - .borrow_mut() - .commands - .add - .push("/bin/ls -l -a".into()); // candidate - r0_task1 - .as_ref() - .borrow_mut() - .commands - .add - .push("/bin/ls .*".into()); // regex args > r1_task1 - - r1_task0 - .as_ref() - .borrow_mut() - .commands - .add - .push("/bin/ls -l -a".into()); //AllCaps > r1_task1 - r1_task1 - .as_ref() - .borrow_mut() - .commands - .add - .push("/bin/ls -l -a".into()); //One Capability > r1_task1 - - r1_task0.as_ref().borrow_mut().cred.capabilities = Some((!CapSet::empty()).into()); - let mut capset = CapSet::empty(); - capset.add(Cap::SYS_ADMIN); - r1_task1.as_ref().borrow_mut().cred.capabilities = Some(capset.into()); - - let cred = Cred { - user: User::from_uid(Uid::from_raw(0)).unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - let result = config.matches(&cred, &None, &command); - debug!("Result : {:?}", result); - assert!(result.is_ok()); - let result = result.unwrap(); - assert_eq!( - result.task().as_ref().borrow().name, - IdTask::Name("role0_task_0".to_string()) - ); - assert_eq!(result.role().as_ref().borrow().name, "role0"); - } - - #[test] - - fn test_setuid_fallback_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setuid` avec un `fallback` - let fallback_user = SUserType::from(get_non_root_uid()); - let chooser_struct = SSetuidSet { - fallback: fallback_user.clone(), - default: SetBehavior::None, - add: vec![], // Pas d'ajout explicite - sub: vec![], // Pas de restriction explicite - }; - task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); - - let cred = Cred::builder().user_name("root").group_name("root").build(); - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - // Exécution du match - let filter_matcher = FilterMatcher::builder().user(fallback_user.clone()).build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - println!("Résultat matches: {:?}", result); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que l'utilisateur assigné est bien celui du fallback - assert_eq!(result.settings.setuid, Some(fallback_user.clone())); - - println!("Test réussi : L'utilisateur spécifié correspond bien au fallback."); - } - - #[test] - - fn test_setuid_fallback_nonarg_valid() { - // Configuration de test - let config = setup_test_config(1); - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setuid` avec un `fallback` - let fallback_user = SUserType::from(get_non_root_uid()); - let chooser_struct = SSetuidSet { - fallback: fallback_user.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let result = config.matches(&cred, &None, &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que l'utilisateur assigné est bien celui du fallback - assert_eq!(result.settings.setuid, Some(fallback_user.clone())); - - println!("Test réussi : L'utilisateur spécifié correspond bien au fallback lorsqu'aucun utilisateur valide n'est fourni."); - } - - #[test] - - fn test_setuid_add_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setuid` avec un `fallback` - let fallback_user = SUserType::from(get_non_root_uid()); - let chooser_struct = SSetuidSet { - fallback: fallback_user.clone(), - default: SetBehavior::None, - add: vec![SUserType::from("root")], // Ajout d'un utilisateur - sub: vec![], // Pas de restriction explicite - }; - task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder().user("root").build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que l'utilisateur assigné est bien celui de l'ajout - assert_eq!(result.settings.setuid, Some(SUserType::from("root"))); - - println!("Test réussi : L'utilisateur spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setuid_add_sub_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setuid` avec un `fallback` - let fallback_user = SUserType::from(1); - let chooser_struct = SSetuidSet { - fallback: fallback_user.clone(), - default: SetBehavior::None, - add: vec![SUserType::from("root")], // Ajout d'un utilisateur - sub: vec![SUserType::from("root")], // Restriction d'un utilisateur - }; - task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - // Exécution du match - let filter_matcher = FilterMatcher::builder().user("root").build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - println!("Résultat matches: {:?}", result); - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : L'utilisateur spécifié ne correspond pas à la restriction."); - } - - #[test] - fn test_setuid_all_sub_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setuid` avec un `fallback` - let fallback_user = SUserType::from(get_non_root_uid()); - let chooser_struct = SSetuidSet { - fallback: fallback_user.clone(), - default: SetBehavior::All, - add: vec![], - sub: vec![SUserType::from("root")], // Restriction d'un utilisateur - }; - task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - // Exécution du match - let filter_matcher = FilterMatcher::builder().user("root").build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : L'utilisateur spécifié ne correspond pas "); - } - - #[test] - - fn test_setuid_all_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setuid` avec un `fallback` - let fallback_user = SUserType::from(get_non_root_uid()); - let chooser_struct = SSetuidSet { - fallback: fallback_user.clone(), - default: SetBehavior::All, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - // Exécution du match - let filter_matcher = FilterMatcher::builder().user("root").build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que l'utilisateur assigné est autorisé - assert_eq!(result.settings.setuid, Some(SUserType::from("root"))); - - println!("Test réussi : L'utilisateur spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setuid_none_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - // Définition du `setuid` avec un `fallback` - let fallback_user = SUserType::from(get_non_root_uid()); - let chooser_struct = SSetuidSet { - fallback: fallback_user.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - // Exécution du match - let filter_matcher = FilterMatcher::builder().user("root").build(); - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : L'utilisateur spécifié ne correspond pas "); - } - - #[test] - fn test_setuid_all_add_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setuid` avec un `fallback` - let fallback_user = SUserType::from(get_non_root_uid()); - let chooser_struct = SSetuidSet { - fallback: fallback_user.clone(), - default: SetBehavior::All, - add: vec![SUserType::from("root")], // Ajout d'un utilisateur - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - // Exécution du match - let filter_matcher = FilterMatcher::builder().user("root").build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que l'utilisateur assigné est bien celui de l'ajout - assert_eq!(result.settings.setuid, Some(SUserType::from("root"))); - - println!("Test réussi : L'utilisateur spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setuid_none_add_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - // Définition du `setuid` avec un `fallback` - let fallback_user = SUserType::from(get_non_root_uid()); - let chooser_struct = SSetuidSet { - fallback: fallback_user.clone(), - default: SetBehavior::None, - add: vec![SUserType::from("root")], // Ajout d'un utilisateur - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setuid = Some(SUserChooser::ChooserStruct(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - // Exécution du match - let filter_matcher = FilterMatcher::builder().user("nouser").build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : L'utilisateur spécifié ne correspond pas "); - } - - #[test] - fn test_equal_settings() { - let mut settings1 = ExecSettings::new(); - let mut settings2 = ExecSettings::new(); - assert_eq!(settings1, settings2); - settings1.exec_path = PathBuf::from("/bin/ls"); - assert_ne!(settings1, settings2); - settings2.exec_path = PathBuf::from("/bin/ls"); - assert_eq!(settings1, settings2); - settings1.exec_args = vec!["-l".to_string()]; - assert_ne!(settings1, settings2); - settings2.exec_args = vec!["-l".to_string()]; - assert_eq!(settings1, settings2); - settings1.setuid = Some("root".into()); - assert_ne!(settings1, settings2); - settings2.setuid = Some("root".into()); - assert_eq!(settings1, settings2); - settings1.setgroups = Some(SGroups::Single("root".into())); - assert_ne!(settings1, settings2); - settings2.setgroups = Some(SGroups::Single("root".into())); - assert_eq!(settings1, settings2); - settings1.caps = Some(CapSet::empty()); - assert_ne!(settings1, settings2); - settings2.caps = Some(CapSet::empty()); - assert_eq!(settings1, settings2); - } - - #[test] - fn test_two_task_matches_equals() { - let config = rc_refcell!(SConfig::default()); - let role = rc_refcell!(SRole::default()); - role.as_ref().borrow_mut()._config = Some(Rc::downgrade(&config)); - role.as_ref().borrow_mut().name = "test".to_string(); - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - let mut task1 = STask::default(); - let mut task2 = STask::default(); - task1.name = IdTask::Name("task1".to_string()); - task2.name = IdTask::Name("task2".to_string()); - task1.commands.add.push("/bin/ls".into()); - task2.commands.add.push("/bin/ls".into()); - task1.options = Some(Rc::new(RefCell::new(Opt::default()))); - task2.options = Some(Rc::new(RefCell::new(Opt::default()))); - task1._role = Some(Rc::downgrade(&role)); - task2._role = Some(Rc::downgrade(&role)); - task1.cred.capabilities = Some((!CapSet::empty()).into()); - task2.cred.capabilities = Some((!CapSet::empty()).into()); - role.as_ref().borrow_mut().tasks.push(Rc::new(task1.into())); - role.as_ref().borrow_mut().tasks.push(Rc::new(task2.into())); - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - let command = vec!["/bin/ls".to_string()]; - let result = role.matches(&cred, &None, &command); - assert!(result.is_ok()); - role.as_ref().borrow_mut()[0] - .as_ref() - .borrow_mut() - .options - .as_mut() - .unwrap() - .as_ref() - .borrow_mut() - .path - .as_mut() - .unwrap() - .add - .replace(["/test".to_string()].iter().cloned().collect()); - let result = role.matches(&cred, &None, &command); - assert!(result.is_err()); - } - - #[test] - fn test_two_role_default() { - let config: Versioning>> = - serde_json::from_str(&fs::read_to_string("../resources/rootasrole.json").unwrap()) - .unwrap(); - let config = config.data; - make_weak_config(&config); - config.as_ref().borrow_mut()[0].as_ref().borrow_mut().actors[0] = - SActor::user("root").build(); - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - let command = vec!["/bin/ls".to_string()]; - let result = config.matches(&cred, &None, &command); - assert!(result.is_ok()); - // must match the r_root role and t_root task - let result = result.unwrap(); - assert_eq!(result.role().as_ref().borrow().name, "r_root"); - assert_eq!( - result.task().as_ref().borrow().name, - IdTask::Name("t_root".to_string()) - ); - let command = vec!["/usr/bin/chsr".to_string(), "show".to_string()]; - let result = config.matches(&cred, &None, &command); - assert!(result.is_ok()); - // must match the r_root role and t_chsr task - let result = result.unwrap(); - assert_eq!(result.role().as_ref().borrow().name, "r_root"); - assert_eq!( - result.task().as_ref().borrow().name, - IdTask::Name("t_chsr".to_string()) - ); - } - #[test] - - fn test_schooseruser_setuid_types() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .actor(SActor::user("root").build()) - .task( - STask::builder(1) - .cred( - SCredentials::builder() - .setuid(SUserChooser::ChooserStruct( - SSetuidSet::builder(SUserType::from(0), SetBehavior::None) - .build(), - )) - .build(), - ) - .commands( - SCommands::builder(SetBehavior::None) - .add(["/bin/ls".into()]) - .build(), - ) - .build(), - ) - .task( - STask::builder(2) - .cred(SCredentials::builder().setuid("root").build()) - .commands( - SCommands::builder(SetBehavior::None) - .add(["/bin/pwd".into()]) - .build(), - ) - .build(), - ) - .task( - STask::builder(3) - .cred(SCredentials::builder().setuid(0).build()) - .commands( - SCommands::builder(SetBehavior::None) - .add(["/bin/cat".into()]) - .build(), - ) - .build(), - ) - .build(), - ) - .build(); - - // Vérifier si les tâches existent avant d’appeler unwrap() - - let t1 = config - .task("test", 1) - .expect(" Erreur : La tâche 1 n'existe pas !"); - let t2 = config - .task("test", 2) - .expect(" Erreur : La tâche 2 n'existe pas !"); - let t3 = config - .task("test", 3) - .expect(" Erreur : La tâche 3 n'existe pas !"); - - // Affichage pour debug - println!("Tâche 1 : {:?}", t1); - println!("Tâche 2 : {:?}", t2); - println!("Tâche 3 : {:?}", t3); - - let cred = Cred::builder().user_name("root").group_name("root").build(); - - let chooser_struct2 = SUserType::from("root"); - let chooser_struct3 = SUserType::from(0); - - let command1 = vec!["/bin/ls".to_string()]; - let command2 = vec!["/bin/pwd".to_string()]; - let command3 = vec!["/bin/cat".to_string()]; - - let filter_matcher = FilterMatcher::builder().user("root").build(); - let result = config.matches(&cred, &Some(filter_matcher), &command1); - assert!(result.is_ok(), "Erreur : L !"); - let result1 = config.matches(&cred, &None, &command1); - assert!(result1.is_ok(), "Erreur : La tâche 1 ne correspond pas !"); - let result2 = config.matches(&cred, &None, &command2); - assert!(result2.is_ok(), "Erreur : La tâche 2 ne correspond pas !"); - let result3 = config.matches(&cred, &None, &command3); - assert!(result3.is_ok(), "Erreur : La tâche 3 ne correspond pas !"); - - let result1 = result1.unwrap(); - let result2 = result2.unwrap(); - let result3 = result3.unwrap(); - - assert_eq!(result1.settings.setuid, Some(SUserType::from(0))); - assert_eq!(result1.settings.task.upgrade(), Some(t1.clone())); - println!( - " Test réussi : L'utilisateur spécifié correspond bien à l'ajout pour la tâche 1." - ); - - assert_eq!(result2.settings.setuid, Some(chooser_struct2)); - assert_eq!(result2.settings.task.upgrade(), Some(t2.clone())); - println!( - " Test réussi : L'utilisateur spécifié correspond bien à l'ajout pour la tâche 2." - ); - - assert_eq!(result3.settings.setuid, Some(chooser_struct3)); - assert_eq!(result3.settings.task.upgrade(), Some(t3.clone())); - println!( - " Test réussi : L'utilisateur spécifié correspond bien à l'ajout pour la tâche 3." - ); - } - - #[test] - fn test_setgid_fallback_single_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(fallback_group.clone()) - .build(); - - // Exécution du match - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que le groupe assigné est bien celui de fallback - assert_eq!(result.settings.setgroups, Some(fallback_group.clone())); - - println!("Test successful: The specified user correctly matches the fallback."); - } - - #[test] - fn test_setgid_fallback_multiple_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::Multiple(vec![ - SGroupType::from(get_non_root_gid(0).unwrap()), - SGroupType::from(get_non_root_gid(1).unwrap()), - ]); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::Multiple(vec![ - SGroupType::from(get_non_root_gid(0).unwrap()), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .build(); - - // Exécution du match - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que le groupe assigné est bien celui de fallaback - assert_eq!(result.settings.setgroups, Some(fallback_group.clone())); - - println!("Test successful: The specified user correctly matches the fallback."); - } - - #[test] - fn test_setgid_fallback_nonarg_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let result = config.matches(&cred, &None, &command); - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que le groupe assigné est bien celui de fallback - assert_eq!(result.settings.setgroups, Some(fallback_group.clone())); - - println!("Test successful: The specified group correctly matches the fallback when no valid group is provided."); - } - - #[test] - fn test_setgid_add_single_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::from("root")], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::from("root")) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que le groupe assigné est bien celui de l'ajout - assert_eq!(result.settings.setgroups, Some(SGroups::from("root"))); - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_add_multiple_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::Multiple(vec![ - SGroupType::from(0), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::Multiple(vec![ - SGroupType::from(0), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que le groupe assigné est bien celui de l'ajout - assert_eq!( - result.settings.setgroups, - Some(SGroups::Multiple(vec![ - SGroupType::from(0), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - ); - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_add_sub_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::from("root")], - sub: vec![SGroups::from("root")], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::from("root")) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_all_sub_single_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![], - sub: vec![SGroups::from("root")], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::from("root")) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_all_sub_multiple_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![], - sub: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_all_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::from("root")) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que le groupe assigné est autorisé - assert_eq!(result.settings.setgroups, Some(SGroups::from("root"))); - - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_none_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::from("root")) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_all_add_single_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![SGroups::from("root")], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::from("root")) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que le groupe assigné est bien celui de l'ajout - assert_eq!(result.settings.setgroups, Some(SGroups::from("root"))); - - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_all_add_multiple_valid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::All, - add: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_ok()); - let result = result.unwrap(); - - // Vérification que le groupe assigné est bien celui de l'ajout - assert_eq!( - result.settings.setgroups, - Some(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - ); - - println!("Test réussi : Le groupe spécifié correspond bien à l'ajout."); - } - - #[test] - fn test_setgid_none_add_single_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::None); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::from("root")], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .user(SUserType::from(get_non_root_gid(1).unwrap())) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_none_add_multiple_invvalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::Multiple(vec![ - SGroupType::from(get_non_root_gid(0).unwrap()), - SGroupType::from("root"), - ])) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_add_multiple_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::from("root")) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_add_single_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::from("root")], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(0).unwrap()), - ])) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_add_diff_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(0).unwrap()), - ])) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_setgid_add_list_or_invalid() { - // Configuration de test - let config = setup_test_config(1); // Un seul rôle pour simplifier - let role = setup_test_role(1, Some(config.as_ref().borrow().roles[0].clone()), None); - let task = role.as_ref().borrow().tasks[0].clone(); - - // Ajout d'un acteur autorisé - role.as_ref() - .borrow_mut() - .actors - .push(SActor::user("root").build()); - - task.as_ref().borrow_mut().commands.default_behavior = Some(SetBehavior::All); - - // Définition du `setgid` avec un `fallback` - let fallback_group = SGroups::from(get_non_root_gid(0).unwrap()); - let chooser_struct = SSetgidSet { - fallback: fallback_group.clone(), - default: SetBehavior::None, - add: vec![ - SGroups::from("root"), - SGroups::from(get_non_root_gid(1).unwrap()), - ], - sub: vec![], - }; - task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::StructChooser(chooser_struct)); - - let cred = Cred { - user: User::from_name("root").unwrap().unwrap(), - groups: vec![Group::from_name("root").unwrap().unwrap()], - ppid: Pid::from_raw(0), - tty: None, - }; - - // Commande de test - let command = vec!["/bin/ls".to_string(), "-l".to_string(), "-a".to_string()]; - - // Exécution du match - let filter_matcher = FilterMatcher::builder() - .group(SGroups::Multiple(vec![ - SGroupType::from("root"), - SGroupType::from(get_non_root_gid(1).unwrap()), - ])) - .build(); - - let result = config.matches(&cred, &Some(filter_matcher), &command); - - // Vérification que le match est réussi - assert!(result.is_err()); - let result = result.unwrap_err(); - - // Vérification que l'erreur est bien de type `NoMatch` - assert!(result.is_no_match()); - - println!("Test réussi : Le groupe spécifié ne correspond pas "); - } - - #[test] - fn test_sgroupschooser_from() { - let sgroup = SGroups::from(get_non_root_gid(0).unwrap()); - let sgroupschooser = SGroupschooser::from(sgroup.clone()); - assert_eq!(sgroupschooser, SGroupschooser::Group(sgroup.clone())); - let chooser_struct = SSetgidSet { - fallback: sgroup.clone(), - default: SetBehavior::None, - add: vec![ - SGroups::from("root"), - SGroups::from(get_non_root_gid(1).unwrap()), - ], - sub: vec![], - }; - let sgroupschooser = SGroupschooser::from(chooser_struct.clone()); - assert_eq!( - sgroupschooser, - SGroupschooser::StructChooser(chooser_struct) - ); - let group = "grp"; - let sgroupschooser = SGroupschooser::from(group); - assert_eq!(sgroupschooser, SGroupschooser::Group(group.into())); - let group = 0; - let sgroupschooser = SGroupschooser::from(group); - assert_eq!(sgroupschooser, SGroupschooser::Group(group.into())); - } -} diff --git a/rar-common/src/database/migration.rs b/rar-common/src/database/migration.rs index f6df3bec..93f125e8 100644 --- a/rar-common/src/database/migration.rs +++ b/rar-common/src/database/migration.rs @@ -3,7 +3,7 @@ use std::error::Error; use log::debug; use semver::Version; -use crate::version::PACKAGE_VERSION; +use crate::PACKAGE_VERSION; type MigrationFn = fn(&Migration, &mut T) -> Result<(), Box>; @@ -37,6 +37,7 @@ impl Migration { to: &Version, ) -> Result> { debug!("Checking migration from {} to {} :", self.from(), self.to()); + #[cfg(not(tarpaulin_include))] debug!( " \tself.from() == *from -> {}\tself.from() == *to -> {} diff --git a/rar-common/src/database/mod.rs b/rar-common/src/database/mod.rs index 80f41673..91061751 100644 --- a/rar-common/src/database/mod.rs +++ b/rar-common/src/database/mod.rs @@ -1,30 +1,24 @@ -use std::path::Path; -use std::{cell::RefCell, error::Error, rc::Rc}; - -use crate::save_settings; -use crate::util::{toggle_lock_config, ImmutableLock}; -use crate::version::PACKAGE_VERSION; +use std::error::Error; use actor::{SGroups, SUserType}; use bon::{builder, Builder}; use chrono::Duration; use linked_hash_set::LinkedHashSet; -use log::debug; use options::EnvBehavior; -use serde::{de, Deserialize, Serialize}; +use serde::{de::Deserialize, de::Deserializer, Serialize}; -use self::{migration::Migration, options::EnvKey, structs::SConfig, versionning::Versioning}; +use self::options::EnvKey; -use crate::util::warn_if_mutable; -use crate::SettingsFile; -use crate::{open_with_privileges, write_json_config}; -use crate::{util::immutable_effective, RemoteStorageSettings, ROOTASROLE}; +#[cfg(feature = "finder")] +pub mod score; pub mod actor; -#[cfg(feature = "finder")] -pub mod finder; +//#[cfg(feature = "finder")] +//pub mod finder; +pub mod de; pub mod migration; pub mod options; +pub mod ser; pub mod structs; pub mod versionning; @@ -34,125 +28,10 @@ pub struct FilterMatcher { pub role: Option, pub task: Option, pub env_behavior: Option, - #[builder(into)] - pub user: Option, - pub group: Option, -} - -pub fn make_weak_config(config: &Rc>) { - for role in &config.as_ref().borrow().roles { - role.as_ref().borrow_mut()._config = Some(Rc::downgrade(config)); - for task in &role.as_ref().borrow().tasks { - task.as_ref().borrow_mut()._role = Some(Rc::downgrade(role)); - } - } -} - -pub fn read_json_config>( - settings: Rc>, - settings_path: P, -) -> Result>, Box> { - let default_remote: RemoteStorageSettings = RemoteStorageSettings::default(); - let binding = settings.as_ref().borrow(); - let path = binding - .storage - .settings - .as_ref() - .unwrap_or(&default_remote) - .path - .as_ref(); - if path.is_none() || path.is_some_and(|p| p == settings_path.as_ref()) { - make_weak_config(&settings.as_ref().borrow().config); - return Ok(settings.as_ref().borrow().config.clone()); - } else { - let file = open_with_privileges(path.unwrap())?; - warn_if_mutable( - &file, - settings - .as_ref() - .borrow() - .storage - .settings - .as_ref() - .unwrap_or(&default_remote) - .immutable - .unwrap_or(true), - )?; - let versionned_config: Versioning>> = serde_json::from_reader(file)?; - let config = versionned_config.data; - if let Ok(true) = Migration::migrate( - &versionned_config.version, - &mut *config.as_ref().borrow_mut(), - versionning::JSON_MIGRATIONS, - ) { - save_json(settings.clone(), config.clone())?; - } else { - debug!("No migrations needed"); - } - make_weak_config(&config); - Ok(config) - } -} - -pub fn save_json( - settings: Rc>, - config: Rc>, -) -> Result<(), Box> { - let default_remote: RemoteStorageSettings = RemoteStorageSettings::default(); - let into = ROOTASROLE.into(); - let binding = settings.as_ref().borrow(); - let path = binding - .storage - .settings - .as_ref() - .unwrap_or(&default_remote) - .path - .as_ref() - .unwrap_or(&into); - if path == &into { - // 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!("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, ImmutableLock::Unset)?; - } - } - write_sconfig(&settings.as_ref().borrow(), versionned)?; - 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, ImmutableLock::Set)?; - } - } - debug!("Resetting immutable privilege"); - immutable_effective(false)?; - Ok(()) -} - -fn write_sconfig( - settings: &SettingsFile, - config: Versioning>>, -) -> Result<(), Box> { - let default_remote = RemoteStorageSettings::default(); - let binding = ROOTASROLE.into(); - let path = settings - .storage - .settings - .as_ref() - .unwrap_or(&default_remote) - .path - .as_ref() - .unwrap_or(&binding); - write_json_config(&config, path)?; - Ok(()) + #[builder(with = |s: impl Into| -> Result<_,String> { s.into().fetch_id().ok_or("This user does not exist".into()) })] + pub user: Option, + #[builder(with = |s: impl Into| -> Result<_,String> { s.into().try_into() })] + pub group: Option>, } // deserialize the linked hash set @@ -160,7 +39,7 @@ fn lhs_deserialize_envkey<'de, D>( deserializer: D, ) -> Result>, D::Error> where - D: de::Deserializer<'de>, + D: Deserializer<'de>, { if let Ok(v) = Vec::::deserialize(deserializer) { Ok(Some(v.into_iter().collect())) @@ -188,7 +67,7 @@ where // deserialize the linked hash set fn lhs_deserialize<'de, D>(deserializer: D) -> Result>, D::Error> where - D: de::Deserializer<'de>, + D: Deserializer<'de>, { if let Ok(v) = Vec::::deserialize(deserializer) { Ok(Some(v.into_iter().collect())) @@ -214,7 +93,7 @@ pub fn is_default(t: &T) -> bool { t == &T::default() } -fn serialize_duration(value: &Option, serializer: S) -> Result +pub fn serialize_duration(value: &Option, serializer: S) -> Result where S: serde::Serializer, { @@ -230,23 +109,30 @@ where } } -fn deserialize_duration<'de, D>(deserializer: D) -> Result, D::Error> +pub fn deserialize_duration<'de, D>(deserializer: D) -> Result, D::Error> where - D: de::Deserializer<'de>, + D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; + match convert_string_to_duration(&s) { + Ok(d) => Ok(d), + Err(e) => Err(serde::de::Error::custom(e)), + } +} + +fn convert_string_to_duration(s: &String) -> Result, Box> { let mut parts = s.split(':'); //unwrap or error if let (Some(hours), Some(minutes), Some(seconds)) = (parts.next(), parts.next(), parts.next()) { - let hours: i64 = hours.parse().map_err(de::Error::custom)?; - let minutes: i64 = minutes.parse().map_err(de::Error::custom)?; - let seconds: i64 = seconds.parse().map_err(de::Error::custom)?; + let hours: i64 = hours.parse()?; + let minutes: i64 = minutes.parse()?; + let seconds: i64 = seconds.parse()?; return Ok(Some( Duration::hours(hours) + Duration::minutes(minutes) + Duration::seconds(seconds), )); } - Err(de::Error::custom("Invalid duration format")) + Err("Invalid duration format".into()) } fn serialize_capset(value: &capctl::CapSet, serializer: S) -> Result @@ -261,16 +147,14 @@ where mod tests { use super::*; - struct LinkedHashSetTester(LinkedHashSet); + struct LinkedHashSetTester(pub Option>); impl<'de> Deserialize<'de> for LinkedHashSetTester { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - Ok(Self( - lhs_deserialize_envkey(deserializer).map(|v| v.unwrap())?, - )) + Ok(Self(lhs_deserialize_envkey(deserializer)?)) } } @@ -279,7 +163,7 @@ mod tests { where S: serde::Serializer, { - lhs_serialize_envkey(&Some(self.0.clone()), serializer) + lhs_serialize_envkey(&self.0, serializer) } } @@ -288,7 +172,7 @@ mod tests { where D: serde::Deserializer<'de>, { - Ok(Self(lhs_deserialize(deserializer).map(|v| v.unwrap())?)) + Ok(Self(lhs_deserialize(deserializer)?)) } } @@ -297,20 +181,18 @@ mod tests { where S: serde::Serializer, { - lhs_serialize(&Some(self.0.clone()), serializer) + lhs_serialize(&self.0, serializer) } } - struct DurationTester(Duration); + struct DurationTester(Option); impl<'de> Deserialize<'de> for DurationTester { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - Ok(Self( - deserialize_duration(deserializer).map(|v| v.unwrap())?, - )) + Ok(Self(deserialize_duration(deserializer)?)) } } @@ -319,7 +201,7 @@ mod tests { where S: serde::Serializer, { - serialize_duration(&Some(self.0.clone()), serializer) + serialize_duration(&self.0, serializer) } } @@ -328,21 +210,11 @@ mod tests { let json = r#"["key1", "key2", "key3"]"#; let deserialized: Option> = serde_json::from_str(json).unwrap(); assert!(deserialized.is_some()); - let set = deserialized.unwrap(); - assert_eq!(set.0.len(), 3); - assert!(set.0.contains(&EnvKey::from("key1"))); - assert!(set.0.contains(&EnvKey::from("key2"))); - assert!(set.0.contains(&EnvKey::from("key3"))); - } - - #[test] - fn test_lhs_serialize_envkey() { - let mut set = LinkedHashSetTester(LinkedHashSet::new()); - set.0.insert(EnvKey::from("key1")); - set.0.insert(EnvKey::from("key2")); - set.0.insert(EnvKey::from("key3")); - let serialized = serde_json::to_string(&Some(set)).unwrap(); - assert_eq!(serialized, r#"["key1","key2","key3"]"#); + let set = deserialized.unwrap().0.unwrap(); + assert_eq!(set.len(), 3); + assert!(set.contains(&EnvKey::from("key1"))); + assert!(set.contains(&EnvKey::from("key2"))); + assert!(set.contains(&EnvKey::from("key3"))); } #[test] @@ -350,26 +222,26 @@ mod tests { let json = r#"["value1", "value2", "value3"]"#; let deserialized: Option> = serde_json::from_str(json).unwrap(); assert!(deserialized.is_some()); - let set = deserialized.unwrap(); - assert_eq!(set.0.len(), 3); - assert!(set.0.contains("value1")); - assert!(set.0.contains("value2")); - assert!(set.0.contains("value3")); + let set = deserialized.unwrap().0.unwrap(); + assert_eq!(set.len(), 3); + assert!(set.contains("value1")); + assert!(set.contains("value2")); + assert!(set.contains("value3")); } #[test] fn test_lhs_serialize() { - let mut set = LinkedHashSetTester(LinkedHashSet::new()); - set.0.insert("value1".to_string()); - set.0.insert("value2".to_string()); - set.0.insert("value3".to_string()); + let mut set = LinkedHashSetTester(Some(LinkedHashSet::new())); + set.0.as_mut().unwrap().insert("value1".to_string()); + set.0.as_mut().unwrap().insert("value2".to_string()); + set.0.as_mut().unwrap().insert("value3".to_string()); let serialized = serde_json::to_string(&Some(set)).unwrap(); assert_eq!(serialized, r#"["value1","value2","value3"]"#); } #[test] fn test_serialize_duration() { - let duration = Some(DurationTester(Duration::seconds(3661))); + let duration = DurationTester(Some(Duration::seconds(3661))); let serialized = serde_json::to_string(&duration).unwrap(); assert_eq!(serialized, r#""01:01:01""#); } @@ -377,10 +249,10 @@ mod tests { #[test] fn test_deserialize_duration() { let json = r#""01:01:01""#; - let deserialized: Option = serde_json::from_str(json).unwrap(); - assert!(deserialized.is_some()); - let duration = deserialized.unwrap(); - assert_eq!(duration.0.num_seconds(), 3661); + let deserialized: DurationTester = serde_json::from_str(json).unwrap(); + assert!(deserialized.0.is_some()); + let duration = deserialized.0.unwrap(); + assert_eq!(duration.num_seconds(), 3661); } #[test] @@ -390,4 +262,81 @@ mod tests { assert!(!is_default(&1)); assert!(!is_default(&"non-default".to_string())); } + #[test] + fn test_lhs_serialize_empty() { + let set: LinkedHashSetTester = LinkedHashSetTester(None); + let serialized = serde_json::to_string(&Some(set)).unwrap(); + assert_eq!(serialized, r#"null"#); + let set: LinkedHashSetTester = LinkedHashSetTester(None); + let serialized = serde_json::to_string(&Some(set)).unwrap(); + assert_eq!(serialized, r#"null"#); + let duration = DurationTester(None); + let serialized = serde_json::to_string(&duration).unwrap(); + assert_eq!(serialized, r#"null"#); + } + #[test] + fn test_lhs_deserialize_envkey_null() { + let json = r#"null"#; + let deserialized: Option> = serde_json::from_str(json).unwrap(); + assert!(deserialized.is_none()); + } + + #[test] + fn test_lhs_deserialize_empty_object() { + let json = r#"{}"#; + let deserialized: Result>, _> = + serde_json::from_str(json); + assert!(deserialized.is_err()); + } + + #[test] + fn test_lhs_serialize_empty_set() { + let set = LinkedHashSetTester(Some(LinkedHashSet::::new())); + let serialized = serde_json::to_string(&Some(set)).unwrap(); + assert_eq!(serialized, r#"[]"#); + } + + #[test] + fn test_serialize_duration_large() { + let duration = Some(DurationTester(Some(Duration::seconds(3600 * 25 + 61)))); + let serialized = serde_json::to_string(&duration).unwrap(); + assert_eq!(serialized, r#""25:01:01""#); + } + + #[test] + fn test_deserialize_duration_leading_zeros() { + let json = r#""001:002:003""#; + let deserialized: DurationTester = serde_json::from_str(json).unwrap(); + assert!(deserialized.0.is_some()); + let duration = deserialized.0.unwrap(); + assert_eq!(duration.num_seconds(), 3723); + } + + #[test] + fn test_deserialize_duration_with_spaces() { + let json = r#"" 01:01:01 ""#; + let deserialized: Result = serde_json::from_str(json); + assert!(deserialized.is_err()); + } + + #[test] + fn test_deserialize_duration_non_numeric() { + let json = r#""aa:bb:cc""#; + let deserialized: Result = serde_json::from_str(json); + assert!(deserialized.is_err()); + } + + #[test] + fn test_deserialize_duration_invalid() { + let json = r#""test""#; + let deserialized: Result = serde_json::from_str(json); + assert!(deserialized.is_err()); + } + + #[test] + fn test_lhs_deserialize_envkey_mixed_types() { + let json = r#"["key1", 123, null]"#; + let deserialized: Result, _> = serde_json::from_str(json); + assert!(deserialized.is_err()); + } } diff --git a/rar-common/src/database/options.rs b/rar-common/src/database/options.rs index 9c50595e..0d4a9014 100644 --- a/rar-common/src/database/options.rs +++ b/rar-common/src/database/options.rs @@ -1,37 +1,38 @@ use std::collections::HashMap; -#[cfg(feature = "finder")] -use std::path::PathBuf; use std::{borrow::Borrow, cell::RefCell, rc::Rc}; +use std::{env, result::Result}; use bon::{bon, builder, Builder}; use chrono::Duration; -#[cfg(feature = "finder")] -use libc::PATH_MAX; +use konst::eq_str; use linked_hash_set::LinkedHashSet; #[cfg(feature = "pcre2")] use pcre2::bytes::Regex; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; -use strum::{Display, EnumIs, EnumIter, FromRepr}; +use strum::{Display, EnumIs, EnumIter, EnumString, FromRepr}; use log::debug; -#[cfg(feature = "finder")] -use log::warn; use crate::rc_refcell; -#[cfg(feature = "finder")] -use super::finder::Cred; -use super::{deserialize_duration, is_default, serialize_duration, FilterMatcher}; +//#[cfg(feature = "finder")] +//use super::finder::Cred; +//#[cfg(feature = "finder")] +//use super::finder::SecurityMin; +use super::{ + convert_string_to_duration, deserialize_duration, is_default, serialize_duration, FilterMatcher, +}; use super::{ lhs_deserialize, lhs_deserialize_envkey, lhs_serialize, lhs_serialize_envkey, structs::{SConfig, SRole, STask}, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] +#[repr(u8)] pub enum Level { #[default] None, @@ -51,9 +52,13 @@ pub enum OptType { Timeout, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum PathBehavior { Delete, KeepSafe, @@ -62,9 +67,13 @@ pub enum PathBehavior { Inherit, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display, EnumString, +)] +#[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum TimestampType { #[default] PPID, @@ -112,15 +121,19 @@ pub struct SPathOptions { )] #[builder(with = |v : impl IntoIterator| { v.into_iter().map(|s| s.to_string()).collect() })] pub sub: Option>, - #[serde(default)] - #[serde(flatten)] - #[builder(default)] - pub _extra_fields: Map, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)] +// ...existing code... +impl SPathOptions {} +// ...existing code... + +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum EnvBehavior { Delete, Keep, @@ -155,13 +168,13 @@ pub struct SEnvOptions { pub default_behavior: EnvBehavior, #[serde(alias = "override", default, skip_serializing_if = "Option::is_none")] pub override_behavior: Option, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - #[builder(default, with = |iter: impl IntoIterator| { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[builder(with = |iter: impl IntoIterator| { let mut map = HashMap::with_hasher(Default::default()); map.extend(iter.into_iter().map(|(k, v)| (k.to_string(), v.to_string()))); map })] - pub set: HashMap, + pub set: Option>, #[serde( default, skip_serializing_if = "Option::is_none", @@ -191,34 +204,46 @@ pub struct SEnvOptions { pub _extra_fields: Map, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum SBounding { Strict, - Ignore, #[default] Inherit, + Ignore, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] #[serde(rename_all = "kebab-case")] #[derive(Default)] +#[repr(u8)] pub enum SPrivileged { - Privileged, #[default] User, Inherit, + Privileged, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy)] +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] #[serde(rename_all = "kebab-case")] #[derive(Default)] +#[repr(u8)] pub enum SAuthentication { - Skip, #[default] Perform, Inherit, + Skip, } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] @@ -257,8 +282,8 @@ impl Opt { #[builder(into)] wildcard_denied: Option, timeout: Option, #[builder(default)] _extra_fields: Map, - ) -> Rc> { - rc_refcell!(Opt { + ) -> Self { + Opt { level, path, env, @@ -268,79 +293,49 @@ impl Opt { wildcard_denied, timeout, _extra_fields, - }) - } - - pub fn raw_new(level: Level) -> Self { - Opt { - level, - ..Default::default() } } - pub fn level_default() -> Rc> { + pub fn level_default() -> Self { Self::builder(Level::Default) - .root(SPrivileged::User) - .bounding(SBounding::Strict) - .path( - SPathOptions::builder(PathBehavior::Delete) - .add([ - "/usr/local/sbin", - "/usr/local/bin", - "/usr/sbin", - "/usr/bin", - "/sbin", - "/snap/bin", - ]) - .build(), - ) - .authentication(SAuthentication::Perform) + .maybe_root(env!("RAR_USER_CONSIDERED").parse().ok()) + .maybe_bounding(env!("RAR_BOUNDING").parse().ok()) + .path(SPathOptions::level_default()) + .maybe_authentication(env!("RAR_AUTHENTICATION").parse().ok()) .env( - SEnvOptions::builder(EnvBehavior::Delete) - .keep([ - "HOME", - "USER", - "LOGNAME", - "COLORS", - "DISPLAY", - "HOSTNAME", - "KRB5CCNAME", - "LS_COLORS", - "PS1", - "PS2", - "XAUTHORY", - "XAUTHORIZATION", - "XDG_CURRENT_DESKTOP", - ]) - .unwrap() - .check([ - "COLORTERM", - "LANG", - "LANGUAGE", - "LC_*", - "LINGUAS", - "TERM", - "TZ", - ]) - .unwrap() - .delete([ - "PS4", - "SHELLOPTS", - "PERLLIB", - "PERL5LIB", - "PERL5OPT", - "PYTHONINSPECT", - ]) - .unwrap() - .build(), + SEnvOptions::builder( + env!("RAR_ENV_DEFAULT") + .parse() + .unwrap_or(EnvBehavior::Delete), + ) + .keep(env!("RAR_ENV_KEEP_LIST").split(',').collect::>()) + .unwrap() + .check(env!("RAR_ENV_CHECK_LIST").split(',').collect::>()) + .unwrap() + .delete( + env!("RAR_ENV_DELETE_LIST") + .split(',') + .collect::>(), + ) + .unwrap() + .set( + serde_json::from_str(env!("RAR_ENV_SET_LIST")) + .unwrap_or_else(|_| Map::default()), + ) + .maybe_override_behavior(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().ok()) + .build(), ) .timeout( STimeout::builder() - .type_field(TimestampType::PPID) - .duration(Duration::minutes(5)) + .maybe_type_field(env!("RAR_TIMEOUT_TYPE").parse().ok()) + .maybe_duration( + convert_string_to_duration(&env!("RAR_TIMEOUT_DURATION").to_string()) + .ok() + .flatten(), + ) .build(), ) - .wildcard_denied(";&|") + .wildcard_denied(env!("RAR_WILDCARD_DENIED")) .build() } } @@ -361,28 +356,33 @@ impl Default for Opt { } } -impl Default for OptStack { - fn default() -> Self { - OptStack { - stack: [None, Some(Opt::level_default()), None, None, None], - roles: None, - role: None, - task: None, - } - } -} - impl Default for SPathOptions { fn default() -> Self { SPathOptions { default_behavior: PathBehavior::Inherit, add: None, sub: None, - _extra_fields: Map::default(), } } } +impl SPathOptions { + pub fn level_default() -> Self { + SPathOptions::builder( + env!("RAR_PATH_DEFAULT") + .parse() + .unwrap_or(PathBehavior::Delete), + ) + .add(env!("RAR_PATH_ADD_LIST").split(':').collect::>()) + .sub( + env!("RAR_PATH_REMOVE_LIST") + .split(':') + .collect::>(), + ) + .build() + } +} + fn is_valid_env_name(s: &str) -> bool { let mut chars = s.chars(); @@ -401,12 +401,12 @@ fn is_valid_env_name(s: &str) -> bool { #[cfg(feature = "pcre2")] fn is_regex(s: &str) -> bool { - Regex::new(s).is_ok() + Regex::new(&format!("^{}$", s)).is_ok() } #[cfg(not(feature = "pcre2"))] fn is_regex(_s: &str) -> bool { - true // Always return true if regex feature is disabled + false // Always return true if regex feature is disabled } impl EnvKey { @@ -465,28 +465,10 @@ impl<'de> Deserialize<'de> for EnvKey { } } -impl SEnvOptions { - pub fn new(behavior: EnvBehavior) -> Self { - SEnvOptions { - default_behavior: behavior, - ..Default::default() - } - } -} - trait EnvSet { fn env_matches(&self, wildcarded: &EnvKey) -> bool; } -impl EnvSet for HashMap { - fn env_matches(&self, wildcarded: &EnvKey) -> bool { - match wildcarded.env_type { - EnvKeyType::Normal => self.contains_key(&wildcarded.value), - EnvKeyType::Wildcarded => self.keys().any(|s| check_wildcarded(wildcarded, s)), - } - } -} - impl EnvSet for LinkedHashSet { fn env_matches(&self, needle: &EnvKey) -> bool { self.iter().any(|s| match s.env_type { @@ -515,61 +497,89 @@ fn check_wildcarded(_wildcarded: &EnvKey, _s: &String) -> bool { true } -#[cfg(feature = "finder")] -fn tz_is_safe(tzval: &str) -> bool { - // tzcode treats a value beginning with a ':' as a path. - let tzval = if let Some(val) = tzval.strip_prefix(':') { - val - } else { - tzval - }; +#[derive(Debug, PartialEq)] +pub struct ConstParseError(pub &'static str); +use std::fmt::{self, Display}; - // Reject fully-qualified TZ that doesn't begin with the zoneinfo dir. - if tzval.starts_with('/') { - return false; +impl Display for ConstParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!( + "Failed to parse the const {} defined in .cargo/config.toml", + self.0 + )) } +} - // Make sure TZ only contains printable non-space characters - // and does not contain a '..' path element. - let mut lastch = '/'; - for cp in tzval.chars() { - if cp.is_ascii_whitespace() || !cp.is_ascii_graphic() { - return false; +impl ConstParseError { + const fn panic(&self) -> ! { + panic!("failed to parse a const") + } +} + +impl PathBehavior { + pub const fn try_parse(input: &str) -> std::result::Result { + match input { + _ if eq_str(input, "delete") => Ok(PathBehavior::Delete), + _ if eq_str(input, "keep_safe") => Ok(PathBehavior::KeepSafe), + _ if eq_str(input, "keep_unsafe") => Ok(PathBehavior::KeepUnsafe), + _ if eq_str(input, "inherit") => Ok(PathBehavior::Inherit), + _ => ConstParseError("PathBehavior").panic(), } - if lastch == '/' - && cp == '.' - && tzval - .chars() - .nth(tzval.chars().position(|c| c == '.').unwrap() + 1) - == Some('.') - && (tzval - .chars() - .nth(tzval.chars().position(|c| c == '.').unwrap() + 2) - == Some('/') - || tzval - .chars() - .nth(tzval.chars().position(|c| c == '.').unwrap() + 2) - .is_none()) - { - return false; + } +} + +impl EnvBehavior { + pub const fn try_parse(input: &str) -> std::result::Result { + match input { + _ if eq_str(input, "delete") => Ok(EnvBehavior::Delete), + _ if eq_str(input, "keep") => Ok(EnvBehavior::Keep), + _ if eq_str(input, "inherit") => Ok(EnvBehavior::Inherit), + _ => ConstParseError("EnvBehavior").panic(), + } + } +} + +impl SPrivileged { + pub const fn try_parse(input: &str) -> std::result::Result { + match input { + _ if eq_str(input, "user") => Ok(SPrivileged::User), + _ if eq_str(input, "inherit") => Ok(SPrivileged::Inherit), + _ if eq_str(input, "privileged") => Ok(SPrivileged::Privileged), + _ => ConstParseError("SPrivileged").panic(), } - lastch = cp; } +} - // Reject extra long TZ values (even if not a path). - if tzval.len() >= >::try_into(PATH_MAX).unwrap() { - return false; +impl TimestampType { + pub const fn try_parse(input: &str) -> std::result::Result { + match input { + _ if eq_str(input, "ppid") => Ok(TimestampType::PPID), + _ if eq_str(input, "tty") => Ok(TimestampType::TTY), + _ if eq_str(input, "uid") => Ok(TimestampType::UID), + _ => ConstParseError("TimestampType").panic(), + } } +} - true +impl SBounding { + pub const fn try_parse(input: &str) -> std::result::Result { + match input { + _ if eq_str(input, "strict") => Ok(SBounding::Strict), + _ if eq_str(input, "inherit") => Ok(SBounding::Inherit), + _ if eq_str(input, "ignore") => Ok(SBounding::Ignore), + _ => ConstParseError("SBounding").panic(), + } + } } -#[cfg(feature = "finder")] -fn check_env(key: &str, value: &str) -> bool { - debug!("Checking env: {}={}", key, value); - match key { - "TZ" => tz_is_safe(value), - _ => !value.chars().any(|c| c == '/' || c == '%'), +impl SAuthentication { + pub const fn try_parse(input: &str) -> std::result::Result { + match input { + _ if eq_str(input, "perform") => Ok(SAuthentication::Perform), + _ if eq_str(input, "inherit") => Ok(SAuthentication::Inherit), + _ if eq_str(input, "skip") => Ok(SAuthentication::Skip), + _ => ConstParseError("SAuthentication").panic(), + } } } @@ -646,72 +656,7 @@ impl OptStackBuilder { } fn with_default(self) -> Self { - self.opt(Some( - Opt::builder(Level::Default) - .root(SPrivileged::User) - .bounding(SBounding::Strict) - .path( - SPathOptions::builder(PathBehavior::Delete) - .add([ - "/usr/local/sbin", - "/usr/local/bin", - "/usr/sbin", - "/usr/bin", - "/sbin", - "/bin", - "/snap/bin", - ]) - .build(), - ) - .authentication(SAuthentication::Perform) - .env( - SEnvOptions::builder(EnvBehavior::Delete) - .keep([ - "HOME", - "USER", - "LOGNAME", - "COLORS", - "DISPLAY", - "HOSTNAME", - "KRB5CCNAME", - "LS_COLORS", - "PS1", - "PS2", - "XAUTHORY", - "XAUTHORIZATION", - "XDG_CURRENT_DESKTOP", - ]) - .unwrap() - .check([ - "COLORTERM", - "LANG", - "LANGUAGE", - "LC_*", - "LINGUAS", - "TERM", - "TZ", - ]) - .unwrap() - .delete([ - "PS4", - "SHELLOPTS", - "PERLLIB", - "PERL5LIB", - "PERL5OPT", - "PYTHONINSPECT", - ]) - .unwrap() - .build(), - ) - .timeout( - STimeout::builder() - .type_field(TimestampType::TTY) - .duration(Duration::minutes(5)) - .build(), - ) - .wildcard_denied(";&|") - .build(), - )) + self.opt(Some(rc_refcell!(Opt::level_default()))) } } @@ -762,44 +707,6 @@ impl OptStack { } } - #[cfg(feature = "finder")] - fn calculate_path(&self) -> String { - let path = self.get_final_path(); - let default = LinkedHashSet::new(); - if let Some(add) = path.add { - let final_add = add.difference(path.sub.as_ref().unwrap_or(&default)).fold( - "".to_string(), - |mut acc, s| { - if !acc.is_empty() { - acc.insert(0, ':'); - } - acc.insert_str(0, s); - acc - }, - ); - match path.default_behavior { - PathBehavior::Inherit | PathBehavior::Delete => final_add, - is_safe => std::env::vars() - .find_map(|(key, value)| if key == "PATH" { Some(value) } else { None }) - .unwrap_or(String::new()) - .split(':') - .filter(|s| { - !path.sub.as_ref().unwrap_or(&default).contains(*s) - && (!is_safe.is_keep_safe() || PathBuf::from(s).exists()) - }) - .fold(final_add, |mut acc, s| { - if !acc.is_empty() { - acc.push(':'); - } - acc.push_str(s); - acc - }), - } - } else { - "".to_string() - } - } - fn get_final_path(&self) -> SPathOptions { let mut final_behavior = PathBehavior::Delete; let default = LinkedHashSet::new(); @@ -869,156 +776,6 @@ impl OptStack { .build() } - #[allow(dead_code)] - #[cfg(not(tarpaulin_include))] - fn union_all_path(&self) -> SPathOptions { - let mut final_behavior = PathBehavior::Delete; - let default = LinkedHashSet::new(); - let final_add = rc_refcell!(LinkedHashSet::new()); - // Cannot use HashSet as we need to keep order - let final_sub = rc_refcell!(LinkedHashSet::new()); - self.iter_in_options(|opt| { - let final_add_clone = Rc::clone(&final_add); - let final_sub_clone = Rc::clone(&final_sub); - if let Some(p) = opt.path.borrow().as_ref() { - match p.default_behavior { - PathBehavior::Delete => { - let union = final_add_clone - .as_ref() - .borrow() - .union(p.add.as_ref().unwrap_or(&default)) - .filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e)) - .cloned() - .collect(); - // policy is to delete, so we add whitelist and remove blacklist - final_add_clone.as_ref().replace(union); - debug!("delete final_add: {:?}", final_add_clone.as_ref().borrow()); - } - PathBehavior::KeepSafe | PathBehavior::KeepUnsafe => { - let union = final_sub_clone - .as_ref() - .borrow() - .union(p.sub.as_ref().unwrap_or(&default)) - .filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e)) - .cloned() - .collect(); - //policy is to keep, so we remove blacklist and add whitelist - final_sub_clone.as_ref().replace(union); - } - PathBehavior::Inherit => { - if final_behavior.is_delete() { - let union: LinkedHashSet = final_add_clone - .as_ref() - .borrow() - .union(p.add.as_ref().unwrap_or(&default)) - .filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e)) - .cloned() - .collect(); - final_add_clone.as_ref().borrow_mut().extend(union); - debug!("inherit final_add: {:?}", final_add_clone.as_ref().borrow()); - } else { - let union: LinkedHashSet = final_sub_clone - .as_ref() - .borrow() - .union(p.sub.as_ref().unwrap_or(&default)) - .filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e)) - .cloned() - .collect(); - final_sub_clone.as_ref().borrow_mut().extend(union); - } - } - } - if !p.default_behavior.is_inherit() { - final_behavior = p.default_behavior; - } - } - }); - SPathOptions::builder(final_behavior) - .add( - final_add - .clone() - .as_ref() - .borrow() - .iter() - .collect::>() - .as_slice(), - ) - .sub( - final_sub - .clone() - .as_ref() - .borrow() - .iter() - .collect::>() - .as_slice(), - ) - .build() - } - - #[cfg(feature = "finder")] - pub fn calculate_filtered_env( - &self, - opt_filter: Option, - target: Cred, - final_env: I, - ) -> Result, String> - where - I: Iterator, - { - let env = self.get_final_env(opt_filter); - if env.default_behavior.is_keep() { - warn!("Keeping environment variables is dangerous operation, it can lead to security vulnerabilities. - Please consider using delete instead. - See https://www.sudo.ws/security/advisories/bash_env/, - https://www.sudo.ws/security/advisories/perl_env/ or - https://nvd.nist.gov/vuln/detail/CVE-2006-0151"); - } - let mut final_env: HashMap = match env.default_behavior { - EnvBehavior::Inherit => Err("Internal Error with environment behavior".to_string()), - EnvBehavior::Delete => Ok(final_env - .filter_map(|(key, value)| { - let key = EnvKey::new(key).expect("Unexpected environment variable"); - if env.keep.env_matches(&key) - || (env.check.env_matches(&key) && check_env(&key.value, &value)) - { - debug!("Keeping env: {}={}", key.value, value); - Some((key.value, value)) - } else { - debug!("Dropping env: {}", key.value); - None - } - }) - .collect()), - EnvBehavior::Keep => Ok(final_env - .filter_map(|(key, value)| { - let key = EnvKey::new(key).expect("Unexpected environment variable"); - if !env.delete.env_matches(&key) - || (env.check.env_matches(&key) && check_env(&key.value, &value)) - { - debug!("Keeping env: {}={}", key.value, value); - Some((key.value, value)) - } else { - debug!("Dropping env: {}", key.value); - None - } - }) - .collect()), - }?; - final_env.insert("PATH".into(), self.calculate_path()); - final_env.insert("LOGNAME".into(), target.user.name.clone()); - final_env.insert("USER".into(), target.user.name); - final_env.insert("HOME".into(), target.user.dir.to_string_lossy().to_string()); - final_env - .entry("TERM".into()) - .or_insert_with(|| "unknown".into()); - final_env.insert( - "SHELL".into(), - target.user.shell.to_string_lossy().to_string(), - ); - final_env.extend(env.set); - Ok(final_env) - } - fn get_final_env(&self, cmd_filter: Option) -> SEnvOptions { let mut final_behavior = EnvBehavior::default(); let mut final_set = HashMap::new(); @@ -1037,9 +794,9 @@ impl OptStack { .unwrap_or(&LinkedHashSet::new()) .iter() .filter(|e| { - !p.set.env_matches(e) - || !p.check.env_matches(e) - || !p.delete.env_matches(e) + //p.set.as_ref().is_some_and(|set| !set.env_matches(e)) || + + !p.check.env_matches(e) || !p.delete.env_matches(e) }) .cloned() .collect(); @@ -1048,7 +805,11 @@ impl OptStack { .as_ref() .unwrap_or(&LinkedHashSet::new()) .iter() - .filter(|e| !p.set.env_matches(e) || !p.delete.env_matches(e)) + .filter(|e| { + //p.set.as_ref().is_some_and(|set| !set.env_matches(e)) + //|| + !p.delete.env_matches(e) + }) .cloned() .collect(); final_delete = p @@ -1056,10 +817,15 @@ impl OptStack { .as_ref() .unwrap_or(&LinkedHashSet::new()) .iter() - .filter(|e| !p.set.env_matches(e) || !p.check.env_matches(e)) + .filter(|e| { + //p.set.as_ref().is_some_and(|set| !set.env_matches(e)) || + !p.check.env_matches(e) + }) .cloned() .collect(); - final_set = p.set.clone(); + if let Some(set) = &p.set { + final_set = set.clone(); + } debug!("check: {:?}", final_check); p.default_behavior } @@ -1076,7 +842,9 @@ impl OptStack { .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new())) .cloned() .collect(); - final_set.extend(p.set.clone()); + if let Some(set) = &p.set { + final_set.extend(set.clone()); + } debug!("check: {:?}", final_check); final_behavior } @@ -1094,130 +862,6 @@ impl OptStack { .build() } - #[allow(dead_code)] - #[cfg(not(tarpaulin_include))] - fn union_all_env( - &self, - ) -> ( - EnvBehavior, - LinkedHashSet, - LinkedHashSet, - LinkedHashSet, - ) { - let mut final_behavior = EnvBehavior::default(); - let mut final_keep = LinkedHashSet::new(); - let mut final_check = LinkedHashSet::new(); - let mut final_delete = LinkedHashSet::new(); - self.iter_in_options(|opt| { - if let Some(p) = opt.env.borrow().as_ref() { - final_behavior = match p.default_behavior { - EnvBehavior::Delete => { - // policy is to delete, so we add whitelist and remove blacklist - final_keep = final_keep - .union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.check.env_matches(e) || !p.delete.env_matches(e)) - .cloned() - .collect(); - final_check = final_check - .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.delete.env_matches(e)) - .cloned() - .collect(); - p.default_behavior - } - EnvBehavior::Keep => { - //policy is to keep, so we remove blacklist and add whitelist - final_delete = final_delete - .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.keep.env_matches(e) || !p.check.env_matches(e)) - .cloned() - .collect(); - final_check = final_check - .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.keep.env_matches(e)) - .cloned() - .collect(); - p.default_behavior - } - EnvBehavior::Inherit => { - if final_behavior.is_delete() { - final_keep = final_keep - .union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.delete.env_matches(e) || !p.check.env_matches(e)) - .cloned() - .collect(); - final_check = final_check - .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.delete.env_matches(e)) - .cloned() - .collect(); - } else { - final_delete = final_delete - .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.keep.env_matches(e) || !p.check.env_matches(e)) - .cloned() - .collect(); - final_check = final_check - .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) - .filter(|e| !p.keep.env_matches(e)) - .cloned() - .collect(); - } - final_behavior - } - }; - } - }); - (final_behavior, final_keep, final_check, final_delete) - } - pub fn get_root_behavior(&self) -> (Level, SPrivileged) { - self.find_in_options(|opt| { - if let Some(p) = &opt.borrow().root { - return Some((opt.level, *p)); - } - None - }) - .unwrap_or((Level::None, SPrivileged::default())) - } - pub fn get_bounding(&self) -> (Level, SBounding) { - self.find_in_options(|opt| { - if let Some(p) = &opt.borrow().bounding { - return Some((opt.level, *p)); - } - None - }) - .unwrap_or((Level::None, SBounding::default())) - } - pub fn get_authentication(&self) -> (Level, SAuthentication) { - self.find_in_options(|opt| { - if let Some(p) = &opt.borrow().authentication { - return Some((opt.level, *p)); - } - None - }) - .unwrap_or((Level::None, SAuthentication::default())) - } - - pub fn get_wildcard(&self) -> (Level, String) { - self.find_in_options(|opt| { - if let Some(p) = opt.borrow().wildcard_denied.borrow().as_ref() { - return Some((opt.level, p.clone())); - } - None - }) - .unwrap_or((Level::None, "".to_owned())) - } - - pub fn get_timeout(&self) -> (Level, STimeout) { - self.find_in_options(|opt| { - if let Some(p) = &opt.borrow().timeout { - return Some((opt.level, p.clone())); - } - None - }) - .unwrap_or((Level::None, STimeout::default())) - } - fn get_level(&self) -> Level { let (level, _) = self .find_in_options(|opt| Some((opt.level, ()))) @@ -1226,7 +870,7 @@ impl OptStack { } pub fn to_opt(&self) -> Rc> { - Opt::builder(self.get_level()) + rc_refcell!(Opt::builder(self.get_level()) .path(self.get_final_path()) .env(self.get_final_env(None)) .maybe_root( @@ -1257,76 +901,13 @@ impl OptStack { self.find_in_options(|opt| opt.timeout.clone().map(|timeout| (opt.level, timeout))) .map(|(_, timeout)| timeout), ) - .build() - } -} - -impl PartialEq for OptStack { - fn eq(&self, other: &Self) -> bool { - // we must assess that every option result in the same final result - let path = self.get_final_path(); - let default = LinkedHashSet::new(); - let other_path = other.get_final_path(); - let res = path.default_behavior == other_path.default_behavior - && path - .add - .as_ref() - .unwrap_or(&default) - .symmetric_difference(other_path.add.as_ref().unwrap_or(&default)) - .count() - == 0 - && path - .sub - .as_ref() - .unwrap_or(&default) - .symmetric_difference(other_path.sub.as_ref().unwrap_or(&default)) - .count() - == 0 - && self.get_root_behavior().1 == other.get_root_behavior().1 - && self.get_bounding().1 == other.get_bounding().1 - && self.get_wildcard().1 == other.get_wildcard().1 - && self.get_authentication().1 == other.get_authentication().1 - && self.get_timeout().1 == other.get_timeout().1; - debug!( - "final_behavior == other_path.behavior : {} - && add {:?} - other_add {:?} == 0 : {} - && sub - other_sub == 0 : {} - && self.get_root_behavior().1 == other.get_root_behavior().1 : {} - && self.get_bounding().1 == other.get_bounding().1 : {} - && self.get_wildcard().1 == other.get_wildcard().1 : {} - && self.get_authentication().1 == other.get_authentication().1 : {} - && self.get_timeout().1 == other.get_timeout().1 : {}", - path.default_behavior == other_path.default_behavior, - path.add, - other_path.add, - path.add - .as_ref() - .unwrap_or(&default) - .symmetric_difference(other_path.add.as_ref().unwrap_or(&default)) - .count() - == 0, - path.sub - .as_ref() - .unwrap_or(&default) - .symmetric_difference(other_path.sub.as_ref().unwrap_or(&default)) - .count() - == 0, - self.get_root_behavior().1 == other.get_root_behavior().1, - self.get_bounding().1 == other.get_bounding().1, - self.get_wildcard().1 == other.get_wildcard().1, - self.get_authentication().1 == other.get_authentication().1, - self.get_timeout().1 == other.get_timeout().1 - ); - debug!("OPT check: {}", res); - res + .build()) } } #[cfg(test)] mod tests { - use nix::unistd::Pid; - use super::super::options::*; use super::super::structs::*; @@ -1394,94 +975,6 @@ mod tests { ); } - #[cfg(feature = "finder")] - #[test] - fn test_get_path() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .options(|opt| { - opt.path( - SPathOptions::builder(PathBehavior::Inherit) - .add(["path2"]) - .build(), - ) - .build() - }) - .build(), - ) - .options(|opt| { - opt.path( - SPathOptions::builder(PathBehavior::Delete) - .add(["path1"]) - .build(), - ) - .build() - }) - .build(); - let options = OptStack::from_role(config.as_ref().borrow().roles.first().unwrap().clone()); - let res = options.calculate_path(); - assert_eq!(res, "path2:path1"); - } - - #[cfg(feature = "finder")] - #[test] - fn test_get_path_delete() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .options(|opt| { - opt.path( - SPathOptions::builder(PathBehavior::Delete) - .add(["path2"]) - .build(), - ) - .build() - }) - .build(), - ) - .options(|opt| { - opt.path( - SPathOptions::builder(PathBehavior::Delete) - .add(["path1"]) - .build(), - ) - .build() - }) - .build(); - let options = OptStack::from_role(config.role("test").unwrap()).calculate_path(); - assert!(options.contains("path2")); - } - - #[cfg(feature = "finder")] - #[test] - fn test_opt_add_sub() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .options(|opt| { - opt.path( - SPathOptions::builder(PathBehavior::Delete) - .sub(["path1"]) - .build(), - ) - .build() - }) - .build(), - ) - .options(|opt| { - opt.path( - SPathOptions::builder(PathBehavior::Delete) - .add(["path1"]) - .build(), - ) - .build() - }) - .build(); - let options = OptStack::from_role(config.role("test").unwrap()).calculate_path(); - assert!(!options.contains("path1")); - } - #[test] fn test_env_global_to_task() { let config = SConfig::builder() @@ -1649,6 +1142,19 @@ mod tests { .clone(), vec![EnvKey::from("env2")] )); + assert_eq!( + global_options + .env + .as_ref() + .unwrap() + .keep + .as_ref() + .unwrap_or(&LinkedHashSet::new()) + .iter() + .map(|e| e.clone().into()) + .collect::>(), + vec!["env2".to_string()] + ); assert_eq!(global_options.root.unwrap(), SPrivileged::Privileged); assert_eq!(global_options.bounding.unwrap(), SBounding::Ignore); assert_eq!( @@ -1758,166 +1264,24 @@ mod tests { } #[test] - fn test_get_timeout() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .options(|opt| { - opt.timeout(STimeout::builder().duration(Duration::minutes(5)).build()) - .build() - }) - .build(), - ) - .options(|opt| { - opt.timeout( - STimeout::builder() - .type_field(TimestampType::PPID) - .duration(Duration::minutes(10)) - .build(), - ) - .build() - }) - .build(); - let options = OptStack::from_role(config.role("test").unwrap()).get_timeout(); - assert_eq!(options.1.duration.unwrap(), Duration::minutes(5)); - assert_eq!(options.0, Level::Role); - assert!(options.1.type_field.is_none()); - } - - #[test] - fn test_get_root_behavior() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .task(STask::builder(1).build()) - .options(|opt| opt.root(SPrivileged::User).build()) - .build(), - ) - .options(|opt| opt.root(SPrivileged::Privileged).build()) - .build(); - let (level, sprivilege) = - OptStack::from_task(config.task("test", 1).unwrap()).get_root_behavior(); - assert_eq!(level, Level::Role); - assert_eq!(sprivilege, SPrivileged::User); - } - - #[test] - fn test_get_bounding() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .options(|opt| opt.bounding(SBounding::Strict).build()) - .build(), - ) - .options(|opt| opt.bounding(SBounding::Ignore).build()) - .build(); - let (level, bounding) = OptStack::from_role(config.role("test").unwrap()).get_bounding(); - assert_eq!(level, Level::Role); - assert_eq!(bounding, SBounding::Strict); - } - - #[test] - fn test_get_wildcard() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .options(|opt| opt.wildcard_denied("b").build()) - .build(), - ) - .options(|opt| opt.wildcard_denied("a").build()) - .build(); - let (level, wildcard) = OptStack::from_role(config.role("test").unwrap()).get_wildcard(); - assert_eq!(level, Level::Role); - assert_eq!(wildcard, "b"); - } - - #[cfg(feature = "finder")] - #[test] - fn test_tz_is_safe() { - assert!(tz_is_safe("America/New_York")); - assert!(!tz_is_safe("/America/New_York")); - assert!(!tz_is_safe("America/New_York/..")); - //assert path max - assert!(!tz_is_safe( - String::from_utf8(vec![b'a'; (PATH_MAX + 1).try_into().unwrap()]) - .unwrap() - .as_str() - )); - } - - #[cfg(feature = "finder")] - #[test] - fn test_check_env() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .options(|opt| { - opt.env( - SEnvOptions::builder(EnvBehavior::Inherit) - .check(["env2"]) - .unwrap() - .build(), - ) - .build() - }) - .task( - STask::builder(IdTask::Number(1)) - .options(|opt| { - opt.env( - SEnvOptions::builder(EnvBehavior::Inherit) - .keep(["env1"]) - .unwrap() - .build(), - ) - .build() - }) - .build(), - ) - .build(), - ) - .options(|opt| { - opt.env( - SEnvOptions::builder(EnvBehavior::Delete) - .check(["env3"]) - .unwrap() - .set([("env4".to_string(), "value4".to_string())]) - .build(), - ) - .build() - }) - .build(); - let options = OptStack::from_task(config.task("test", 1).unwrap()); - let mut test_env = HashMap::new(); - test_env.insert("env1".to_string(), "value1".to_string()); - test_env.insert("env2".into(), "va%lue2".into()); - test_env.insert("env3".into(), "value3".into()); - let cred = Cred::builder() - .user_id(0) - .group_id(0) - .ppid(Pid::from_raw(0)) - .build(); - let result = options - .calculate_filtered_env(None, cred, test_env.into_iter()) - .unwrap(); - assert_eq!(result.get("env1").unwrap(), "value1"); - assert_eq!(result.get("env3").unwrap(), "value3"); - assert!(result.get("env2").is_none()); - assert_eq!(result.get("env4").unwrap(), "value4"); + fn is_wildcard_env_key() { + assert!(!is_valid_env_name("TEST_.*")); + assert!(!is_valid_env_name("123")); + assert!(!is_valid_env_name("")); + assert!(is_regex("TEST_.*")); } - #[cfg(feature = "finder")] #[test] - fn test_override_env() { + fn test_get_final_env_set_inherit() { let config = SConfig::builder() .role( SRole::builder("test") .task( - STask::builder(IdTask::Number(1)) + STask::builder(1) .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Inherit) - .keep(["env1"]) - .unwrap() + .set([("env1", "value3")]) .build(), ) .build() @@ -1927,8 +1291,7 @@ mod tests { .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Inherit) - .check(["env2"]) - .unwrap() + .set([("env2", "value2")]) .build(), ) .build() @@ -1938,147 +1301,40 @@ mod tests { .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Delete) - .check(["env3"]) - .unwrap() - .set([("env4".to_string(), "value4".to_string())]) + .set([("env1", "value1")]) .build(), ) .build() }) .build(); - - let options = OptStack::from_task(config.task("test", 1).unwrap()); - let mut test_env = HashMap::new(); - test_env.insert("env1".to_string(), "value1".to_string()); - test_env.insert("env2".into(), "va%lue2".into()); - test_env.insert("env3".into(), "value3".into()); - let cred = Cred::builder().user_id(0).group_id(0).build(); - let result = options - .calculate_filtered_env(None, cred, test_env.into_iter()) - .unwrap(); - assert_eq!(result.get("env1").unwrap(), "value1"); - assert_eq!(result.get("env3").unwrap(), "value3"); - assert!(result.get("env2").is_none()); - assert_eq!(result.get("env4").unwrap(), "value4"); - } - - #[test] - fn is_wildcard_env_key() { - assert!(!is_valid_env_name("TEST_.*")); - assert!(!is_valid_env_name("123")); - assert!(!is_valid_env_name("")); - assert!(is_regex("TEST_.*")); - } - - #[test] - fn test_wildcard_env() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .task( - STask::builder(IdTask::Number(1)) - .options(|opt| { - opt.env( - SEnvOptions::builder(EnvBehavior::Delete) - .keep(["TEST_.*"]) - .unwrap() - .build(), - ) - .build() - }) - .build(), - ) - .build(), - ) - .build(); - let options = OptStack::from_task(config.task("test", 1).unwrap()); - let mut test_env = HashMap::new(); - test_env.insert("TEST_A".to_string(), "value1".to_string()); - test_env.insert("TEST_B".into(), "value2".into()); - test_env.insert("TESTaA".into(), "value3".into()); - let cred = Cred::builder().user_id(0).group_id(0).build(); - let result = options - .calculate_filtered_env(None, cred, test_env.into_iter()) - .unwrap(); - assert_eq!(result.get("TEST_A").unwrap(), "value1"); - assert_eq!(result.get("TEST_B").unwrap(), "value2"); - assert!(result.get("TESTaA").is_none()); - } - - #[test] - fn test_safe_path() { - let path = std::env::var("PATH").unwrap(); - - let config = SConfig::builder() - .role( - SRole::builder("test") - .task( - STask::builder(IdTask::Number(1)) - .options(|opt| { - opt.path( - SPathOptions::builder(PathBehavior::KeepSafe) - .add(Vec::::new()) - .sub(["/sys"]) - .build(), - ) - .build() - }) - .build(), - ) - .build(), - ) - .build(); - let options = OptStack::from_task(config.task("test", 1).unwrap()); - std::env::set_var("PATH", "/sys:./proc:/tmp:/bin"); - let res = options.calculate_path(); - - assert_eq!(res, "/tmp:/bin"); - std::env::set_var("PATH", path); - } - - #[test] - fn test_unsafe_path() { - let path = std::env::var("PATH").unwrap(); - - let config = SConfig::builder() - .role( - SRole::builder("test") - .task( - STask::builder(IdTask::Number(1)) - .options(|opt| { - opt.path( - SPathOptions::builder(PathBehavior::KeepUnsafe) - .add(Vec::::new()) - .sub(["/sys"]) - .build(), - ) - .build() - }) - .build(), - ) - .build(), - ) - .build(); - let options = OptStack::from_task(config.task("test", 1).unwrap()); - std::env::set_var("PATH", "/sys:./proc:/tmp:/bin"); - let res = options.calculate_path(); - assert_eq!(res, "./proc:/tmp:/bin"); - std::env::set_var("PATH", path); + let stack = OptStack::from_task(config.task("test", 1).unwrap()); + let opt = stack.to_opt(); + let options = opt.as_ref().borrow(); + assert_eq!( + options + .env + .as_ref() + .unwrap() + .set + .as_ref() + .unwrap_or(&HashMap::new()) + .get("env1") + .unwrap(), + "value3" + ); } #[test] - fn test_inherit_keep_path() { - let path = std::env::var("PATH").unwrap(); + fn test_get_final_path_inherit() { let config = SConfig::builder() .role( SRole::builder("test") .task( - STask::builder(IdTask::Number(1)) + STask::builder(1) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Inherit) - .add(Vec::::new()) - .sub(["/sys"]) + .sub(["/path3"]) .build(), ) .build() @@ -2087,47 +1343,8 @@ mod tests { ) .options(|opt| { opt.path( - SPathOptions::builder(PathBehavior::KeepSafe) - .add(Vec::::new()) - .sub(["/tmp"]) - .build(), - ) - .build() - }) - .build(), - ) - .build(); - let options = OptStack::from_task(config.task("test", 1).unwrap()); - std::env::set_var("PATH", "/sys:./proc:/tmp:/bin"); - let res = options.calculate_path(); - - assert_eq!(res, "/bin"); - std::env::set_var("PATH", path); - } - - #[test] - fn test_final_env_keep() { - let config = SConfig::builder() - .role( - SRole::builder("test") - .task( - STask::builder(IdTask::Number(1)) - .options(|opt| { - opt.env( - SEnvOptions::builder(EnvBehavior::Inherit) - .delete(["env1"]) - .unwrap() - .build(), - ) - .build() - }) - .build(), - ) - .options(|opt| { - opt.env( - SEnvOptions::builder(EnvBehavior::Inherit) - .delete(["env2"]) - .unwrap() + SPathOptions::builder(PathBehavior::Inherit) + .sub(["/path2"]) .build(), ) .build() @@ -2135,78 +1352,68 @@ mod tests { .build(), ) .options(|opt| { - opt.env( - SEnvOptions::builder(EnvBehavior::Keep) - .delete(["env3"]) - .unwrap() + opt.path( + SPathOptions::builder(PathBehavior::KeepSafe) + .sub(["/path1"]) .build(), ) .build() }) .build(); - let options = OptStack::from_task(config.task("test", 1).unwrap()); - let test_env = [ - ("env1", "value1"), - ("env2", "value2"), - ("env3", "value3"), - ("env4", "value4"), - ("env5", "value5"), - ] - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())); - - let cred = Cred::builder().user_id(0).group_id(0).build(); - let result = options - .calculate_filtered_env(None, cred, test_env.into_iter()) - .unwrap(); - assert!(result.get("env1").is_none()); - assert!(result.get("env2").is_none()); - assert!(result.get("env3").is_none()); - assert_eq!(result.get("env4").unwrap(), "value4"); - assert_eq!(result.get("env5").unwrap(), "value5"); + let stack = OptStack::from_task(config.task("test", 1).unwrap()); + let opt = stack.to_opt(); + let options = opt.as_ref().borrow(); + assert!(options + .path + .as_ref() + .unwrap() + .sub + .as_ref() + .unwrap() + .contains("/path1")); + assert!(options + .path + .as_ref() + .unwrap() + .sub + .as_ref() + .unwrap() + .contains("/path2")); + assert!(options + .path + .as_ref() + .unwrap() + .sub + .as_ref() + .unwrap() + .contains("/path3")); } #[test] - fn test_opt_filter_env() { + fn test_find_in_options_none() { let config = SConfig::builder() .role( SRole::builder("test") - .task( - STask::builder(IdTask::Number(1)) - .options(|opt| { - opt.env( - SEnvOptions::builder(EnvBehavior::Delete) - .delete(["envA"]) - .unwrap() - .override_behavior(true) - .build(), - ) - .build() - }) - .build(), - ) + .task(STask::builder(1).build()) .build(), ) .build(); - let options = OptStack::from_task(config.task("test", 1).unwrap()); - let test_env = [("envA", "value1"), ("envB", "value2"), ("envC", "value3")] - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())); - - let cred = Cred::builder().user_id(0).group_id(0).build(); - let result = options - .calculate_filtered_env( - Some( - FilterMatcher::builder() - .env_behavior(EnvBehavior::Keep) - .build(), - ), - cred, - test_env.into_iter(), + let stack = OptStack::from_task(config.task("test", 1).unwrap()); + let res: Option<(Level, SPathOptions)> = stack.find_in_options(|_| None); + assert_eq!(res, None); + } + + #[test] + fn test_invalid_envkey() { + let invalid_env = "3TE(ST_a"; + let env_key = EnvKey::new(invalid_env.to_string()); + assert!(env_key.is_err()); + assert_eq!( + env_key.unwrap_err(), + format!( + "env key {}, must be a valid env, or a valid regex", + invalid_env ) - .unwrap(); - assert!(result.get("envA").is_none()); - assert_eq!(result.get("envB").unwrap(), "value2"); - assert_eq!(result.get("envC").unwrap(), "value3"); + ); } } diff --git a/rar-common/src/database/score.rs b/rar-common/src/database/score.rs new file mode 100644 index 00000000..aa02b64b --- /dev/null +++ b/rar-common/src/database/score.rs @@ -0,0 +1,671 @@ +use std::cmp::Ordering; + +use bon::Builder; +use strum::EnumIs; + +use super::actor::{DGroupType, DGroups, DUserType, SGroupType, SGroups, SUserType}; + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, EnumIs, Default)] +#[repr(u32)] +// Matching user groups for the role +pub enum ActorMatchMin { + UserMatch, + GroupMatch(usize), + #[default] + NoMatch, +} + +impl ActorMatchMin { + pub fn better(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Less + } + pub fn matching(&self) -> bool { + *self != ActorMatchMin::NoMatch + } +} + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] + +// Matching setuid and setgid for the role +pub struct SetuidMin { + is_root: bool, +} + +impl From for SetuidMin { + fn from(s: SUserType) -> Self { + SetuidMin { + is_root: user_is_root(&s), + } + } +} + +impl From<&DUserType<'_>> for SetuidMin { + fn from(s: &DUserType) -> Self { + SetuidMin { + is_root: duser_is_root(s), + } + } +} + +impl From for SetuidMin { + fn from(s: u32) -> Self { + SetuidMin { is_root: s == 0 } + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub struct SetgidMin { + is_root: bool, + nb_groups: usize, +} + +impl From for SetgidMin { + fn from(s: SGroups) -> Self { + SetgidMin { + is_root: groups_contains_root(Some(&s)), + nb_groups: groups_len(Some(&s)), + } + } +} + +impl From<&DGroups<'_>> for SetgidMin { + fn from(s: &DGroups<'_>) -> Self { + SetgidMin { + is_root: dgroups_contains_root(Some(s)), + nb_groups: dgroups_len(Some(&s)), + } + } +} + +impl From<&DGroupType<'_>> for SetgidMin { + fn from(s: &DGroupType<'_>) -> Self { + SetgidMin { + is_root: dgroup_is_root(&s), + nb_groups: 1, + } + } +} + +impl From<&Vec> for SetgidMin { + fn from(s: &Vec) -> Self { + SetgidMin { + is_root: s.iter().any(|id| *id == 0), + nb_groups: s.len(), + } + } +} + +impl PartialOrd for SetgidMin { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for SetgidMin { + fn cmp(&self, other: &Self) -> Ordering { + self.is_root + .cmp(&other.is_root) + .then_with(|| self.nb_groups.cmp(&other.nb_groups)) + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, Default)] +pub struct SetUserMin { + pub uid: Option, + pub gid: Option, +} +impl PartialOrd for SetUserMin { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for SetUserMin { + fn cmp(&self, other: &Self) -> Ordering { + self.uid + .cmp(&other.uid) + .then_with(|| self.gid.cmp(&other.gid)) + } +} + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, Default)] +pub struct CmdMin(u32); + +bitflags::bitflags! { + + impl CmdMin: u32 { + const Match = 0b00001; + const WildcardPath = 0b00010; + const RegexArgs = 0b00100; + const FullRegexArgs = 0b01000; + const FullWildcardPath = 0b10000; + } +} + +impl CmdMin { + pub fn better(&self, other: &Self) -> bool { + (self.matching() && !other.matching()) + || (self.matching() && self.cmp(other) == Ordering::Less) + } + pub fn matching(&self) -> bool { + !self.is_empty() + } +} + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, Default)] +pub enum CapsMin { + #[default] + Undefined, + NoCaps, + CapsNoAdmin(usize), + CapsAdmin(usize), + CapsAll, +} + +#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, Default)] +pub struct SecurityMin(u32); + +bitflags::bitflags! { + + impl SecurityMin: u32 { + const DisableBounding = 0b000001; + const EnableRoot = 0b000010; + const KeepEnv = 0b000100; + const KeepPath = 0b001000; + const KeepUnsafePath = 0b010000; + const SkipAuth = 0b100000; + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, Default, Builder)] +pub struct TaskScore { + #[builder(default)] + pub cmd_min: CmdMin, + #[builder(default)] + pub caps_min: CapsMin, + #[builder(default)] + pub setuser_min: SetUserMin, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug, Default, Builder)] +pub struct Score { + pub user_min: ActorMatchMin, + pub cmd_min: CmdMin, + pub caps_min: CapsMin, + pub setuser_min: SetUserMin, + pub security_min: SecurityMin, +} + +impl Score { + pub fn set_cmd_score(&mut self, cmd_min: CmdMin) { + self.cmd_min = cmd_min; + } + pub fn set_task_score(&mut self, task_score: &TaskScore) { + self.cmd_min = task_score.cmd_min; + self.caps_min = task_score.caps_min; + self.setuser_min = task_score.setuser_min; + } + pub fn set_role_score(&mut self, role_score: &ActorMatchMin) { + self.user_min = *role_score; + } + pub fn prettyprint(&self) -> String { + format!( + "{:?}, {:?}, {:?}, {:?}, {:?}", + self.user_min, self.cmd_min, self.caps_min, self.setuser_min, self.security_min + ) + } + + pub fn user_cmp(&self, other: &Score) -> Ordering { + self.user_min.cmp(&other.user_min) + } + + /// Compare the score of tasks results + pub fn cmd_cmp(&self, other: &Score) -> Ordering { + self.cmd_min + .cmp(&other.cmd_min) + .then(self.caps_min.cmp(&other.caps_min)) + .then(self.setuser_min.cmp(&other.setuser_min)) + .then(self.security_min.cmp(&other.security_min)) + } + + pub fn user_matching(&self) -> bool { + self.user_min != ActorMatchMin::NoMatch + } + + pub fn command_matching(&self) -> bool { + !self.cmd_min.is_empty() + } + + pub fn fully_matching(&self) -> bool { + self.user_matching() && self.command_matching() + } + + /// Return true if the score is better than the other + pub fn better_command(&self, other: &Score) -> bool { + (self.command_matching() && !other.command_matching()) + || (self.command_matching() && self.cmd_cmp(other) == Ordering::Less) + } + + pub fn better_user(&self, other: &Score) -> bool { + (self.user_matching() && !other.user_matching()) + || (self.user_matching() && self.user_cmp(other) == Ordering::Less) + } + + pub fn better_fully(&self, other: &Score) -> bool { + (self.fully_matching() && !other.fully_matching()) + || (self.fully_matching() && self.cmp(other) == Ordering::Less) + } +} + +impl PartialOrd for Score { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Score { + fn cmp(&self, other: &Self) -> Ordering { + self.cmd_cmp(other).then(self.user_cmp(other)) + } + + fn max(self, other: Self) -> Self { + std::cmp::max_by(self, other, Ord::cmp) + } + + fn min(self, other: Self) -> Self { + std::cmp::min_by(self, other, Ord::cmp) + } + + fn clamp(self, min: Self, max: Self) -> Self { + self.max(min).min(max) + } +} + +fn group_is_root(actortype: &SGroupType) -> bool { + (*actortype).fetch_id().map_or(false, |id| id == 0) +} +fn dgroup_is_root(actortype: &DGroupType<'_>) -> bool { + (*actortype).fetch_id().map_or(false, |id| id == 0) +} + +fn user_is_root(actortype: &SUserType) -> bool { + (*actortype).fetch_id().map_or(false, |id| id == 0) +} +fn duser_is_root(actortype: &DUserType<'_>) -> bool { + (*actortype).fetch_id().map_or(false, |id| id == 0) +} + +fn groups_contains_root(list: Option<&SGroups>) -> bool { + if let Some(list) = list { + match list { + SGroups::Single(group) => group_is_root(group), + SGroups::Multiple(groups) => groups.iter().any(group_is_root), + } + } else { + false + } +} + +fn dgroups_contains_root(list: Option<&DGroups<'_>>) -> bool { + if let Some(list) = list { + match list { + DGroups::Single(group) => dgroup_is_root(group), + DGroups::Multiple(groups) => groups.iter().any(dgroup_is_root), + } + } else { + false + } +} + +fn groups_len(groups: Option<&SGroups>) -> usize { + match groups { + Some(groups) => groups.len(), + None => 0, + } +} + +fn dgroups_len(groups: Option<&DGroups<'_>>) -> usize { + match groups { + Some(groups) => groups.len(), + None => 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::database::actor::{DGroupType, DGroups, DUserType, SGroupType, SGroups, SUserType}; + use std::borrow::Cow; + + #[test] + fn test_group_is_root() { + let root_group = SGroupType::from(0); + let non_root_group = SGroupType::from(1); + assert!(group_is_root(&root_group)); + assert!(!group_is_root(&non_root_group)); + } + + #[test] + fn test_dgroup_is_root() { + let root_group = DGroupType::from(0); + let non_root_group = DGroupType::from(1); + assert!(dgroup_is_root(&root_group)); + assert!(!dgroup_is_root(&non_root_group)); + } + + #[test] + fn test_user_is_root() { + let root_user = SUserType::from(0); + let non_root_user = SUserType::from(1); + assert!(user_is_root(&root_user)); + assert!(!user_is_root(&non_root_user)); + } + + #[test] + fn test_duser_is_root() { + let root_user = DUserType::from(0); + let non_root_user = DUserType::from(1); + assert!(duser_is_root(&root_user)); + assert!(!duser_is_root(&non_root_user)); + } + + #[test] + fn test_groups_contains_root() { + let root_group = SGroupType::from(0); + let non_root_group = SGroupType::from(1); + let single = SGroups::Single(root_group.clone()); + let multiple = SGroups::from(vec![non_root_group.clone(), root_group.clone()]); + let none = None; + assert!(groups_contains_root(Some(&single))); + assert!(groups_contains_root(Some(&multiple))); + assert!(!groups_contains_root(Some(&SGroups::Single( + non_root_group + )))); + assert!(!groups_contains_root(none)); + } + + #[test] + fn test_dgroups_contains_root() { + let root_group = DGroupType::from(0); + let non_root_group = DGroupType::from(1); + let single = DGroups::Single(root_group.clone()); + let multiple = + DGroups::Multiple(Cow::Owned(vec![non_root_group.clone(), root_group.clone()])); + let none = None; + assert!(dgroups_contains_root(Some(&single))); + assert!(dgroups_contains_root(Some(&multiple))); + assert!(!dgroups_contains_root(Some(&DGroups::Single( + non_root_group + )))); + assert!(!dgroups_contains_root(none)); + } + + #[test] + fn test_groups_len() { + let group1 = SGroupType::from(0); + let single = SGroups::Single(group1); + let multiple = SGroups::from(vec![SGroupType::from(0), SGroupType::from(1)]); + assert_eq!(groups_len(Some(&single)), 1); + assert_eq!(groups_len(Some(&multiple)), 2); + assert_eq!(groups_len(None), 0); + } + + #[test] + fn test_dgroups_len() { + let group1 = DGroupType::from(0); + let single = DGroups::Single(group1); + let multiple = + DGroups::Multiple(Cow::Owned(vec![DGroupType::from(0), DGroupType::from(1)])); + assert_eq!(dgroups_len(Some(&single)), 1); + assert_eq!(dgroups_len(Some(&multiple)), 2); + assert_eq!(dgroups_len(None), 0); + } + + #[test] + fn test_setgidmin_from_sgroups() { + let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(1)]); + let setgid = SetgidMin::from(groups); + assert!(setgid.is_root); + assert_eq!(setgid.nb_groups, 2); + } + + #[test] + fn test_setgidmin_from_dgroups() { + let groups = DGroups::from(vec![DGroupType::from(1), DGroupType::from(2)]); + let setgid = SetgidMin::from(&groups); + assert!(!setgid.is_root); + assert_eq!(setgid.nb_groups, 2); + } + + #[test] + fn test_setgidmin_from_vec_u32() { + let groups = vec![0, 1, 2]; + let setgid = SetgidMin::from(&groups); + assert!(setgid.is_root); + assert_eq!(setgid.nb_groups, 3); + } + + #[test] + fn test_setgidmin_from_dgrouptype() { + let group = DGroupType::from(0); + let setgid = SetgidMin::from(&group); + assert!(setgid.is_root); + assert_eq!(setgid.nb_groups, 1); + } + + #[test] + fn test_setuidmin_from_susertype() { + let user = SUserType::from(0); + let setuid = SetuidMin::from(user); + assert!(setuid.is_root); + } + + #[test] + fn test_setuidmin_from_dusertype() { + let user = DUserType::from(1); + let setuid = SetuidMin::from(&user); + assert!(!setuid.is_root); + } + + #[test] + fn test_setuidmin_from_u32() { + let setuid = SetuidMin::from(0); + assert!(setuid.is_root); + let setuid = SetuidMin::from(1); + assert!(!setuid.is_root); + } + + #[test] + fn test_score_ordering() { + let mut score1 = Score::default(); + let mut score2 = Score::default(); + score1.cmd_min = CmdMin::from_bits_truncate(0b00001); + score2.cmd_min = CmdMin::from_bits_truncate(0b00010); + assert!(score1 < score2 || score1 == score2 || score1 > score2); + } + + #[test] + fn test_score_prettyprint() { + let score = Score::default(); + let s = score.prettyprint(); + assert!(s.contains("NoMatch")); + } + + #[test] + fn test_cmdmin_better_and_matching() { + let a = CmdMin::from_bits_truncate(0b00001); + let b = CmdMin::from_bits_truncate(0b00000); + assert!(a.matching()); + assert!(!b.matching()); + assert!(!b.better(&a)); + assert!(a.better(&b)); + } + + #[test] + fn test_score_better_methods() { + let mut score1 = Score::default(); + let mut score2 = Score::default(); + score1.cmd_min = CmdMin::from_bits_truncate(0b00001); + score2.cmd_min = CmdMin::from_bits_truncate(0b00000); + assert!(score1.better_command(&score2)); + assert!(!score2.better_command(&score1)); + } + + #[test] + fn test_setuser_min_ordering() { + let setuser1 = SetUserMin { + uid: Some(SetuidMin::from(0)), + gid: Some(SetgidMin::from(&vec![0])), + }; + let setuser2 = SetUserMin { + uid: Some(SetuidMin::from(1)), + gid: Some(SetgidMin::from(&vec![1])), + }; + assert!(setuser1 > setuser2); + } + + #[test] + fn test_setgidmin_ordering() { + let setgid1 = SetgidMin { + is_root: true, + nb_groups: 2, + }; + let setgid2 = SetgidMin { + is_root: false, + nb_groups: 3, + }; + assert!(setgid1 > setgid2); + assert!(setgid2 < setgid1); + assert!(setgid1 != setgid2); + let setgid2 = SetgidMin { + is_root: true, + nb_groups: 3, + }; + assert!(setgid1 < setgid2); + assert!(setgid2 > setgid1); + assert!(setgid1 != setgid2); + } + + #[test] + fn test_actor_match_min() { + let setuser = ActorMatchMin::UserMatch; + assert!(setuser.matching()); + let setuser_other = ActorMatchMin::NoMatch; + assert!(!setuser_other.matching()); + assert!(setuser.better(&setuser_other)); + } + + #[test] + fn test_security_min() { + let security = SecurityMin::empty(); + assert!(security.is_empty()); + let security_other = SecurityMin::DisableBounding; + assert!(!security_other.is_empty()); + assert!(security < security_other); + assert!(security_other > security); + assert!(security_other != security); + let security = SecurityMin::EnableRoot; + assert!(security > security_other); + assert!(security_other < security); + assert!(security_other != security); + let security_other = SecurityMin::KeepEnv; + assert!(security_other > security); + assert!(security < security_other); + assert!(security_other != security); + let security = SecurityMin::KeepPath; + assert!(security > security_other); + assert!(security_other < security); + assert!(security_other != security); + let security_other = SecurityMin::KeepUnsafePath; + assert!(security_other > security); + assert!(security < security_other); + assert!(security_other != security); + let security = SecurityMin::SkipAuth; + assert!(security > security_other); + assert!(security_other < security); + assert!(security_other != security); + let security_other = SecurityMin::empty(); + assert!(security > security_other); + assert!(security_other < security); + } + #[test] + fn test_set_score() { + let mut score = Score::default(); + let task_score = TaskScore { + cmd_min: CmdMin::from_bits_truncate(0b00001), + caps_min: CapsMin::NoCaps, + setuser_min: SetUserMin::default(), + }; + score.set_task_score(&task_score); + assert_eq!(score.cmd_min, CmdMin::from_bits_truncate(0b00001)); + assert_eq!(score.caps_min, CapsMin::NoCaps); + assert_eq!(score.setuser_min, SetUserMin::default()); + let role_score = ActorMatchMin::UserMatch; + score.set_role_score(&role_score); + assert_eq!(score.user_min, ActorMatchMin::UserMatch); + assert_eq!(score.cmd_min, CmdMin::from_bits_truncate(0b00001)); + assert_eq!(score.caps_min, CapsMin::NoCaps); + assert_eq!(score.setuser_min, SetUserMin::default()); + assert_eq!(score.security_min, SecurityMin::empty()); + score.set_cmd_score(CmdMin::from_bits_truncate(0b00010)); + assert_eq!(score.cmd_min, CmdMin::from_bits_truncate(0b00010)); + assert_eq!(score.caps_min, CapsMin::NoCaps); + assert_eq!(score.setuser_min, SetUserMin::default()); + assert_eq!(score.user_min, ActorMatchMin::UserMatch); + assert_eq!(score.security_min, SecurityMin::empty()); + } + + #[test] + fn test_score_matching() { + let mut score = Score::default(); + assert!(!score.user_matching()); + assert!(!score.command_matching()); + assert!(!score.fully_matching()); + score.user_min = ActorMatchMin::UserMatch; + assert!(score.user_matching()); + assert!(!score.command_matching()); + assert!(!score.fully_matching()); + score.cmd_min = CmdMin::from_bits_truncate(0b00001); + assert!(score.user_matching()); + assert!(score.command_matching()); + assert!(score.fully_matching()); + score.user_min = ActorMatchMin::NoMatch; + assert!(!score.user_matching()); + assert!(score.command_matching()); + assert!(!score.fully_matching()); + } + + #[test] + fn test_score_better() { + let mut score1 = Score::default(); + let mut score2 = Score::default(); + score1.cmd_min = CmdMin::from_bits_truncate(0b00001); + score2.cmd_min = CmdMin::from_bits_truncate(0b00010); + assert!(!score2.better_command(&score1)); + assert!(score1.better_command(&score2)); + assert!(!score1.better_user(&score2)); + assert!(!score2.better_user(&score1)); + assert!(!score1.better_fully(&score2)); + assert!(!score2.better_fully(&score1)); + score1.user_min = ActorMatchMin::UserMatch; + score2.user_min = ActorMatchMin::GroupMatch(1); + assert!(score1.better_user(&score2)); + assert!(!score2.better_user(&score1)); + assert!(score1.better_fully(&score2)); + assert!(!score2.better_fully(&score1)); + } + + #[test] + fn test_score_max_min_clamp() { + let mut score1 = Score::default(); + let mut score2 = Score::default(); + score1.cmd_min = CmdMin::from_bits_truncate(0b00001); + score2.cmd_min = CmdMin::from_bits_truncate(0b00010); + assert_eq!(score1.max(score2), score2); + assert_eq!(score2.max(score1), score2); + assert_eq!(score1.min(score2), score1); + assert_eq!(score2.min(score1), score1); + let mut score3 = Score::default(); + score3.cmd_min = CmdMin::from_bits_truncate(0b00011); + assert_eq!(score1.clamp(score2, score3), score2); + assert_eq!(score2.clamp(score1, score3), score2); + } +} diff --git a/rar-common/src/database/ser.rs b/rar-common/src/database/ser.rs new file mode 100644 index 00000000..ddb89e4d --- /dev/null +++ b/rar-common/src/database/ser.rs @@ -0,0 +1,539 @@ +use serde::{ + ser::{SerializeMap, SerializeSeq}, + Serialize, +}; + +use super::{is_default, structs::*}; + +impl Serialize for SConfig { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + if let Some(options) = &self.options { + map.serialize_entry("options", options)?; + } + if !self.roles.is_empty() { + map.serialize_entry("roles", &self.roles)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + if let Some(options) = &self.options { + map.serialize_entry("o", options)?; + } + if !self.roles.is_empty() { + map.serialize_entry("r", &self.roles)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +impl Serialize for SRole { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("name", &self.name)?; + if let Some(options) = &self.options { + map.serialize_entry("options", options)?; + } + if !self.actors.is_empty() { + map.serialize_entry("actors", &self.actors)?; + } + if !self.tasks.is_empty() { + map.serialize_entry("tasks", &self.tasks)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("n", &self.name)?; + if let Some(options) = &self.options { + map.serialize_entry("o", options)?; + } + if !self.actors.is_empty() { + map.serialize_entry("a", &self.actors)?; + } + if !self.tasks.is_empty() { + map.serialize_entry("t", &self.tasks)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +impl Serialize for SetBehavior { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + return serializer.serialize_str(&self.to_string()); + } else { + return serializer.serialize_u8(*self as u8); + } + } +} + +impl Serialize for SSetuidSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("default", &self.default)?; + if let Some(fallback) = &self.fallback { + map.serialize_entry("fallback", fallback)?; + } + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("add", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("del", &v)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("d", &(self.default as u8))?; + if let Some(fallback) = &self.fallback { + map.serialize_entry("f", fallback)?; + } + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("a", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("s", &v)?; + } + map.end() + } + } +} + +impl Serialize for SSetgidSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.default.is_none() && self.sub.is_empty() && self.add.is_empty() { + serializer.serialize_some(&self.fallback) + } else if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("default", &self.default)?; + if !self.fallback.is_empty() { + map.serialize_entry("fallback", &self.fallback)?; + } + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("add", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("del", &v)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("d", &(self.default as u8))?; + if !self.fallback.is_empty() { + map.serialize_entry("f", &self.fallback)?; + } + + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("a", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("s", &v)?; + } + map.end() + } + } +} + +impl Serialize for SCapabilities { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.default_behavior.is_none() && self.sub.is_empty() { + super::serialize_capset(&self.add, serializer) + } else { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(Some(3))?; + if self.default_behavior.is_all() { + map.serialize_entry("default", &self.default_behavior)?; + } + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("add", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("del", &v)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(Some(3))?; + if self.default_behavior.is_all() { + map.serialize_entry("d", &(self.default_behavior as u8))?; + } + if !self.add.is_empty() { + let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("a", &v)?; + } + if !self.sub.is_empty() { + let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); + map.serialize_entry("s", &v)?; + } + map.end() + } + } + } +} + +impl Serialize for SCredentials { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + if self.setuid.is_some() { + map.serialize_entry("setuid", &self.setuid)?; + } + if self.setgid.is_some() { + map.serialize_entry("setgid", &self.setgid)?; + } + if self.capabilities.is_some() { + map.serialize_entry("capabilities", &self.capabilities)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + if self.setuid.is_some() { + map.serialize_entry("u", &self.setuid)?; + } + if self.setgid.is_some() { + map.serialize_entry("g", &self.setgid)?; + } + if self.capabilities.is_some() { + map.serialize_entry("c", &self.capabilities)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +impl Serialize for STask { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("name", &self.name)?; + if let Some(options) = &self.options { + map.serialize_entry("options", options)?; + } + if let Some(purpose) = &self.purpose { + map.serialize_entry("purpose", purpose)?; + } + if !is_default(&self.cred) { + map.serialize_entry("cred", &self.cred)?; + } + if !cmds_is_default(&self.commands) { + map.serialize_entry("commands", &self.commands)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("n", &self.name)?; + if let Some(options) = &self.options { + map.serialize_entry("o", options)?; + } + if let Some(purpose) = &self.purpose { + map.serialize_entry("p", purpose)?; + } + if !is_default(&self.cred) { + map.serialize_entry("i", &self.cred)?; + } + if !cmds_is_default(&self.commands) { + map.serialize_entry("c", &self.commands)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +impl Serialize for SCommands { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.sub.is_empty() && self._extra_fields.is_empty() { + if self.add.is_empty() { + return serializer.serialize_bool( + self.default_behavior + .as_ref() + .is_some_and(|b| *b == SetBehavior::All), + ); + } else if !self.add.is_empty() + && self + .default_behavior + .as_ref() + .is_none_or(|b| *b == SetBehavior::None) + { + let mut seq = serializer.serialize_seq(Some(self.add.len()))?; + for cmd in &self.add { + seq.serialize_element(cmd)?; + } + return seq.end(); + } + } + if serializer.is_human_readable() { + let mut map = serializer.serialize_map(Some(3))?; + if self.default_behavior.is_none() { + map.serialize_entry("default", &self.default_behavior)?; + } + if !self.add.is_empty() { + map.serialize_entry("add", &self.add)?; + } + if !self.sub.is_empty() { + map.serialize_entry("del", &self.sub)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } else { + let mut map = serializer.serialize_map(Some(3))?; + if let Some(behavior) = &self.default_behavior { + map.serialize_entry("d", &(*behavior as u8))?; + } + if !self.add.is_empty() { + map.serialize_entry("a", &self.add)?; + } + if !self.sub.is_empty() { + map.serialize_entry("s", &self.sub)?; + } + for (key, value) in &self._extra_fields { + map.serialize_entry(key, value)?; + } + map.end() + } + } +} + +#[cfg(test)] +mod tests { + use capctl::Cap; + use serde_json::{json, to_value}; + + use crate::database::actor::SActor; + + use super::*; + + #[test] + fn test_sconfig_human_readable() { + let config = SConfig { + options: Some(Default::default()), + roles: vec![], + _extra_fields: Default::default(), + }; + let value = to_value(&config).unwrap(); + assert!(value.get("options").is_some()); + } + + #[test] + fn test_srole_binary() { + let role = SRole::builder("admin") + .actor(SActor::user(0).build()) + .task(STask::builder("test").build()) + .options(|o| { + o.bounding(crate::database::options::SBounding::Ignore) + .build() + }) + .build(); + //cbor4ii encode + let bin: Vec = Vec::new(); + let mut writer = cbor4ii::core::utils::BufWriter::new(bin); + let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); + role.serialize(&mut serializer).unwrap(); + assert!(!writer.buffer().is_empty()); + assert!(!writer + .buffer() + .windows("tasks".len()) + .any(|window| window == "tasks".as_bytes())); + assert!(!writer + .buffer() + .windows("name".len()) + .any(|window| window == "name".as_bytes())); + assert!(!writer + .buffer() + .windows("options".len()) + .any(|window| window == "options".as_bytes())); + assert!(!writer + .buffer() + .windows("actors".len()) + .any(|window| window == "actors".as_bytes())); + } + + #[test] + fn test_setbehavior_serialize() { + let b = SetBehavior::All; + let value = to_value(&b).unwrap(); + assert_eq!(value, json!("all")); + let b = SetBehavior::None; + let bin: Vec = Vec::new(); + let mut writer = cbor4ii::core::utils::BufWriter::new(bin); + let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); + b.serialize(&mut serializer).unwrap(); + assert!(!writer.buffer().is_empty()); + assert!(writer.buffer() == [0x00]); + } + + #[test] + fn test_ssetuidset_human_readable() { + let set = SSetuidSet::builder() + .default(SetBehavior::None) + .fallback(1) + .add(vec![1.into(), 3.into()]) + .sub(vec![4.into(), 5.into()]) + .build(); + let value = to_value(&set).unwrap(); + assert!(value.get("add").is_some()); + } + + #[test] + fn test_ssetgidset_seq() { + let set = SSetgidSet::builder(SetBehavior::None, vec![0, 1]).build(); + let value = to_value(&set).unwrap(); + assert!(value.is_array()); + assert_eq!(value.as_array().unwrap().len(), 2); + assert_eq!(value.as_array().unwrap()[0], json!(0)); + assert_eq!(value.as_array().unwrap()[1], json!(1)); + } + + #[test] + fn test_scapabilities_minimal() { + let caps = SCapabilities::builder(SetBehavior::None) + .add_cap(Cap::SYS_ADMIN) + .build(); + let value = to_value(&caps).unwrap(); + assert!(value.is_array()); + } + + #[test] + fn test_scredentials_human_readable() { + let creds = SCredentials::builder() + .setuid(1) + .setgid(2) + .capabilities( + SCapabilities::builder(SetBehavior::None) + .add_cap(Cap::SYS_ADMIN) + .build(), + ) + .build(); + let value = to_value(&creds).unwrap(); + assert!(value.get("setuid").is_some()); + assert!(value.get("setgid").is_some()); + assert!(value.get("capabilities").is_some()); + } + + #[test] + fn test_stask_binary() { + let task = STask::builder("test") + .options(|o| { + o.bounding(crate::database::options::SBounding::Ignore) + .build() + }) + .cred(SCredentials::builder().setuid(1).setgid(2).build()) + .commands( + SCommands::builder(SetBehavior::All) + .add(vec!["ls".into()]) + .build(), + ) + .build(); + let bin: Vec = Vec::new(); + let mut writer = cbor4ii::core::utils::BufWriter::new(bin); + let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); + task.serialize(&mut serializer).unwrap(); + assert!(!writer.buffer().is_empty()); + assert!(!writer + .buffer() + .windows("name".len()) + .any(|window| window == "name".as_bytes())); + assert!(!writer + .buffer() + .windows("options".len()) + .any(|window| window == "options".as_bytes())); + assert!(!writer + .buffer() + .windows("cred".len()) + .any(|window| window == "cred".as_bytes())); + assert!(!writer + .buffer() + .windows("commands".len()) + .any(|window| window == "commands".as_bytes())); + assert!(writer + .buffer() + .windows("test".len()) + .any(|window| window == "test".as_bytes())); + } + + #[test] + fn test_scommands_bool() { + let cmds = SCommands { + default_behavior: Some(SetBehavior::All), + add: vec![], + sub: vec![], + _extra_fields: Default::default(), + }; + let value = to_value(&cmds).unwrap(); + assert!(value.is_boolean()); + } + + #[test] + fn test_scommands_seq() { + let cmds = SCommands::builder(SetBehavior::None) + .add(vec!["ls".into()]) + .build(); + let value = to_value(&cmds).unwrap(); + assert!(value.is_array()); + } +} diff --git a/rar-common/src/database/structs.rs b/rar-common/src/database/structs.rs index 34a3a637..5f14ac4d 100644 --- a/rar-common/src/database/structs.rs +++ b/rar-common/src/database/structs.rs @@ -1,13 +1,9 @@ use bon::{bon, builder, Builder}; use capctl::{Cap, CapSet}; use derivative::Derivative; -use serde::{ - de::{self, MapAccess, SeqAccess, Visitor}, - ser::SerializeMap, - Deserialize, Deserializer, Serialize, -}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; -use strum::{Display, EnumIs}; +use strum::{Display, EnumIs, EnumString, FromRepr}; use std::{ cell::RefCell, @@ -17,24 +13,20 @@ use std::{ rc::{Rc, Weak}, }; +use crate::rc_refcell; + use super::{ - actor::{SActor, SGroups, SUserType}, - is_default, + actor::{SActor, SGroupType, SGroups, SUserType}, options::{Level, Opt, OptBuilder}, }; -#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)] +#[derive(Deserialize, PartialEq, Eq, Debug)] pub struct SConfig { - #[serde( - default, - skip_serializing_if = "Option::is_none", - deserialize_with = "sconfig_opt" - )] + #[serde(default, deserialize_with = "sconfig_opt", alias = "o")] pub options: Option>>, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default, alias = "r")] pub roles: Vec>>, - #[serde(default)] - #[serde(flatten, skip_serializing_if = "Map::is_empty")] + #[serde(default, flatten)] pub _extra_fields: Map, } @@ -42,15 +34,20 @@ fn sconfig_opt<'de, D>(deserializer: D) -> Result>>, D::E where D: Deserializer<'de>, { - let mut opt = Opt::deserialize(deserializer)?; - opt.level = Level::Global; - Ok(Some(Rc::new(RefCell::new(opt)))) + let opt: Option>> = Option::deserialize(deserializer)?; + if let Some(opt) = opt { + opt.as_ref().borrow_mut().level = Level::Global; + Ok(Some(opt)) + } else { + Ok(None) + } } -#[derive(Serialize, Deserialize, Debug, Derivative)] +#[derive(Deserialize, Debug, Derivative)] #[serde(rename_all = "kebab-case")] #[derivative(PartialEq, Eq)] pub struct SRole { + #[serde(default, skip_serializing_if = "String::is_empty")] pub name: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub actors: Vec, @@ -73,12 +70,16 @@ fn srole_opt<'de, D>(deserializer: D) -> Result>>, D::Err where D: Deserializer<'de>, { - let mut opt = Opt::deserialize(deserializer)?; - opt.level = Level::Role; - Ok(Some(Rc::new(RefCell::new(opt)))) + let opt: Option>> = Option::deserialize(deserializer)?; + if let Some(opt) = opt { + opt.as_ref().borrow_mut().level = Level::Role; + Ok(Some(opt)) + } else { + Ok(None) + } } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] #[serde(untagged)] pub enum IdTask { Name(String), @@ -94,18 +95,28 @@ impl std::fmt::Display for IdTask { } } -#[derive(Serialize, Deserialize, Debug, Derivative)] +pub(super) fn cmds_is_default(cmds: &SCommands) -> bool { + cmds.default_behavior + .as_ref() + .is_none_or(|b| *b == Default::default()) + && cmds.add.is_empty() + && cmds.sub.is_empty() + && cmds._extra_fields.is_empty() +} + +#[derive(Deserialize, Debug, Derivative)] #[derivative(PartialEq, Eq)] pub struct STask { - #[serde(default, skip_serializing_if = "IdTask::is_number")] + #[serde(alias = "n", default, skip_serializing_if = "IdTask::is_number")] pub name: IdTask, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "p", skip_serializing_if = "Option::is_none")] pub purpose: Option, - #[serde(default, skip_serializing_if = "is_default")] + #[serde(alias = "i", default, skip_serializing_if = "is_default")] pub cred: SCredentials, - #[serde(default, skip_serializing_if = "is_default")] + #[serde(alias = "c", default, skip_serializing_if = "cmds_is_default")] pub commands: SCommands, #[serde( + alias = "o", default, skip_serializing_if = "Option::is_none", deserialize_with = "stask_opt" @@ -122,25 +133,26 @@ fn stask_opt<'de, D>(deserializer: D) -> Result>>, D::Err where D: Deserializer<'de>, { - let mut opt = Opt::deserialize(deserializer)?; - opt.level = Level::Task; - Ok(Some(Rc::new(RefCell::new(opt)))) + let opt: Option>> = Option::deserialize(deserializer)?; + if let Some(opt) = opt { + opt.as_ref().borrow_mut().level = Level::Task; + Ok(Some(opt)) + } else { + Ok(None) + } } -#[derive(Serialize, Deserialize, Debug, Builder, PartialEq, Eq)] +#[derive(Deserialize, Debug, Builder, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct SCredentials { - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "u", skip_serializing_if = "Option::is_none")] #[builder(into)] pub setuid: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "g", skip_serializing_if = "Option::is_none")] #[builder(into)] pub setgid: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, alias = "c", skip_serializing_if = "Option::is_none")] pub capabilities: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[builder(into)] - pub additional_auth: Option, // TODO: to extract as plugin #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] #[builder(default)] pub _extra_fields: Map, @@ -177,40 +189,53 @@ impl From for SUserChooser { } } -#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq)] +#[derive(Deserialize, Debug, Clone, Builder, PartialEq, Eq)] pub struct SSetuidSet { - #[builder(start_fn, into)] - pub fallback: SUserType, - #[serde(rename = "default", default, skip_serializing_if = "is_default")] - #[builder(start_fn)] + #[serde( + alias = "d", + rename = "default", + default, + skip_serializing_if = "is_default" + )] + #[builder(default)] pub default: SetBehavior, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[builder(into)] + #[serde(alias = "f", skip_serializing_if = "Option::is_none")] + pub fallback: Option, + #[serde(default, alias = "a", skip_serializing_if = "Vec::is_empty")] #[builder(default, with = FromIterator::from_iter)] pub add: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde( + default, + alias = "del", + alias = "s", + skip_serializing_if = "Vec::is_empty" + )] #[builder(default, with = FromIterator::from_iter)] pub sub: Vec, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Display, Debug, EnumIs, Clone)] -#[serde(rename_all = "lowercase")] +#[derive(PartialEq, Eq, Display, Debug, EnumIs, Clone, Copy, FromRepr, EnumString)] +#[strum(serialize_all = "lowercase")] #[derive(Default)] +#[repr(u8)] pub enum SetBehavior { - All, #[default] None, + All, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(untagged)] pub enum SGroupschooser { - Group(SGroups), + Group(SGroupType), + Groups(SGroups), StructChooser(SSetgidSet), } impl From for SGroupschooser { fn from(group: SGroups) -> Self { - SGroupschooser::Group(group) + SGroupschooser::Groups(group) } } @@ -232,17 +257,23 @@ impl From for SGroupschooser { } } -#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq)] +#[derive(Deserialize, Debug, Clone, Builder, PartialEq, Eq)] pub struct SSetgidSet { - #[builder(start_fn, into)] - pub fallback: SGroups, - #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[serde( + rename = "default", + alias = "d", + default, + skip_serializing_if = "is_default" + )] #[builder(start_fn)] pub default: SetBehavior, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(alias = "f")] + #[builder(start_fn, into)] + pub fallback: SGroups, + #[serde(default, alias = "a", skip_serializing_if = "Vec::is_empty")] #[builder(default, with = FromIterator::from_iter)] pub add: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default, alias = "s", skip_serializing_if = "Vec::is_empty")] #[builder(default, with = FromIterator::from_iter)] pub sub: Vec, } @@ -255,8 +286,6 @@ pub struct SCapabilities { pub add: CapSet, #[builder(field)] pub sub: CapSet, - #[builder(default, with = <_>::from_iter)] - pub _extra_fields: Map, } impl SCapabilitiesBuilder { @@ -278,117 +307,6 @@ impl SCapabilitiesBuilder { } } -impl Serialize for SCapabilities { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - if self.default_behavior.is_none() && self.sub.is_empty() && self._extra_fields.is_empty() { - super::serialize_capset(&self.add, serializer) - } else { - let mut map = serializer.serialize_map(Some(3))?; - if self.default_behavior.is_none() { - map.serialize_entry("default", &self.default_behavior)?; - } - if !self.add.is_empty() { - let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); - map.serialize_entry("add", &v)?; - } - if !self.sub.is_empty() { - let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); - map.serialize_entry("del", &v)?; - } - for (key, value) in &self._extra_fields { - map.serialize_entry(key, value)?; - } - map.end() - } - } -} -impl<'de> Deserialize<'de> for SCapabilities { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct SCapabilitiesVisitor; - - impl<'de> Visitor<'de> for SCapabilitiesVisitor { - type Value = SCapabilities; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an array of strings or a map with SCapabilities fields") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut add = CapSet::default(); - while let Some(cap) = seq.next_element::()? { - add.add(cap.parse().map_err(de::Error::custom)?); - } - - Ok(SCapabilities { - default_behavior: SetBehavior::None, - add, - sub: CapSet::default(), - _extra_fields: Map::new(), - }) - } - - fn visit_map(self, mut map: A) -> Result - where - A: MapAccess<'de>, - { - let mut default_behavior = SetBehavior::None; - let mut add = CapSet::default(); - let mut sub = CapSet::default(); - let mut _extra_fields = Map::new(); - - while let Some(key) = map.next_key::()? { - match key.as_str() { - "default" => { - default_behavior = map - .next_value() - .expect("default entry must be either 'all' or 'none'"); - } - "add" => { - let values: Vec = - map.next_value().expect("add entry must be a list"); - for value in values { - add.add(value.parse().map_err(|_| { - de::Error::custom(format!("Invalid capability: {}", value)) - })?); - } - } - "sub" | "del" => { - let values: Vec = - map.next_value().expect("sub entry must be a list"); - for value in values { - sub.add(value.parse().map_err(|_| { - de::Error::custom(format!("Invalid capability: {}", value)) - })?); - } - } - other => { - _extra_fields.insert(other.to_string(), map.next_value()?); - } - } - } - - Ok(SCapabilities { - default_behavior, - add, - sub, - _extra_fields, - }) - } - } - - deserializer.deserialize_any(SCapabilitiesVisitor) - } -} - #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] #[serde(untagged)] pub enum SCommand { @@ -396,15 +314,11 @@ pub enum SCommand { Complex(Value), } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug)] pub struct SCommands { - #[serde(rename = "default")] pub default_behavior: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] pub add: Vec, - #[serde(default, alias = "del", skip_serializing_if = "Vec::is_empty")] pub sub: Vec, - #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] pub _extra_fields: Map, } @@ -455,7 +369,6 @@ impl Default for SCredentials { setuid: None, setgid: None, capabilities: Some(SCapabilities::default()), - additional_auth: None, _extra_fields: Map::default(), } } @@ -478,14 +391,13 @@ impl Default for SCapabilities { default_behavior: SetBehavior::default(), add: CapSet::empty(), sub: CapSet::empty(), - _extra_fields: Map::default(), } } } impl Default for SSetuidSet { fn default() -> Self { - SSetuidSet::builder(0, SetBehavior::None).build() + SSetuidSet::builder().build() } } @@ -546,7 +458,7 @@ impl SConfig { #[builder] pub fn new( #[builder(field)] roles: Vec>>, - #[builder(with = |f : fn(OptBuilder) -> Rc> | f(Opt::builder(Level::Global)))] + #[builder(with = |f : fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Global))))] options: Option>>, _extra_fields: Option>, ) -> Rc> { @@ -627,6 +539,14 @@ impl SRoleBuilder { self.actors.push(actor); self } + pub fn actors(mut self, actors: impl IntoIterator) -> Self { + self.actors.extend(actors); + self + } + pub fn tasks(mut self, tasks: impl IntoIterator>>) -> Self { + self.tasks.extend(tasks); + self + } } #[bon] @@ -636,7 +556,7 @@ impl SRole { #[builder(start_fn, into)] name: String, #[builder(field)] tasks: Vec>>, #[builder(field)] actors: Vec, - #[builder(with = |f : fn(OptBuilder) -> Rc> | f(Opt::builder(Level::Role)))] + #[builder(with = |f : fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Role))))] options: Option>>, #[builder(default)] _extra_fields: Map, ) -> Rc> { @@ -671,7 +591,7 @@ impl STask { purpose: Option, #[builder(default)] cred: SCredentials, #[builder(default)] commands: SCommands, - #[builder(with = |f : fn(OptBuilder) -> Rc> | f(Opt::builder(Level::Task)))] + #[builder(with = |f : fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Task))))] options: Option>>, #[builder(default)] _extra_fields: Map, _role: Option>>, @@ -737,14 +657,15 @@ impl SCapabilities { } } +/* Confusing impl PartialEq for SUserChooser { fn eq(&self, other: &str) -> bool { match self { SUserChooser::Actor(actor) => actor == &SUserType::from(other), - SUserChooser::ChooserStruct(chooser) => chooser.fallback == *other, + SUserChooser::ChooserStruct(chooser) => chooser.fallback.as_ref().is_some_and(|f| *f == *other), } } -} +}*/ #[cfg(test)] mod tests { @@ -768,7 +689,6 @@ mod tests { #[test] fn test_deserialize() { - println!("START"); let config = r#" { "options": { @@ -834,7 +754,6 @@ mod tests { ] } "#; - println!("STEP 1"); let config: SConfig = serde_json::from_str(config).unwrap(); let options = config.options.as_ref().unwrap().as_ref().borrow(); let path = options.path.as_ref().unwrap(); @@ -892,18 +811,18 @@ mod tests { let role = config.roles[0].as_ref().borrow(); assert_eq!(as_borrow!(role[0]).purpose.as_ref().unwrap(), "purpose1"); let cred = &as_borrow!(&role[0]).cred; - let setuidstruct = SSetuidSet { - fallback: "user1".into(), - default: SetBehavior::All, - add: ["user2".into()].into(), - sub: ["user3".into()].into(), - }; + let setuidstruct = SSetuidSet::builder() + .fallback("user1") + .default(SetBehavior::All) + .add(["user2".into()]) + .sub(["user3".into()]) + .build(); assert!( matches!(cred.setuid.as_ref().unwrap(), SUserChooser::ChooserStruct(set) if set == &setuidstruct) ); assert_eq!( *cred.setgid.as_ref().unwrap(), - SGroupschooser::Group(SGroups::from("setgid1")) + SGroupschooser::Group(SGroupType::from("setgid1")) ); let capabilities = cred.capabilities.as_ref().unwrap(); @@ -969,8 +888,7 @@ mod tests { "capabilities": { "default": "all", "add": ["cap_dac_override"], - "sub": ["cap_dac_override"], - "unknown": "unknown" + "sub": ["cap_dac_override"] }, "unknown": "unknown" }, @@ -994,8 +912,6 @@ mod tests { let binding = config.options.unwrap(); let options = binding.as_ref().borrow(); - let path = options.path.as_ref().unwrap(); - assert_eq!(path._extra_fields.get("unknown").unwrap(), "unknown"); let env = &options.env.as_ref().unwrap(); assert_eq!(env._extra_fields.get("unknown").unwrap(), "unknown"); assert_eq!(options._extra_fields.get("unknown").unwrap(), "unknown"); @@ -1034,11 +950,6 @@ mod tests { let role = config.roles[0].as_ref().borrow(); let cred = &role[0].as_ref().borrow().cred; assert_eq!(cred._extra_fields.get("unknown").unwrap(), "unknown"); - let capabilities = cred.capabilities.as_ref().unwrap(); - assert_eq!( - capabilities._extra_fields.get("unknown").unwrap(), - "unknown" - ); let commands = &as_borrow!(role[0]).commands; assert_eq!(commands._extra_fields.get("unknown").unwrap(), "unknown"); } @@ -1161,7 +1072,7 @@ mod tests { ); assert_eq!( *cred.setgid.as_ref().unwrap(), - SGroupschooser::Group(SGroups::from("setgid1")) + SGroupschooser::Group(SGroupType::from("setgid1")) ); let capabilities = cred.capabilities.as_ref().unwrap(); @@ -1192,12 +1103,14 @@ mod tests { .cred( SCredentials::builder() .setuid(SUserChooser::ChooserStruct( - SSetuidSet::builder("user1", SetBehavior::All) + SSetuidSet::builder() + .fallback("user1") + .default(SetBehavior::All) .add(["user2".into()]) .sub(["user3".into()]) .build(), )) - .setgid(SGroupschooser::Group(SGroups::from("setgid1"))) + .setgid(SGroupschooser::Group(SGroupType::from("setgid1"))) .capabilities( SCapabilities::builder(SetBehavior::All) .add_cap(Cap::NET_BIND_SERVICE) @@ -1245,8 +1158,7 @@ mod tests { .build() }) .build(); - let config = serde_json::to_string_pretty(&config).unwrap(); - println!("{}", config); + serde_json::to_string_pretty(&config).unwrap(); } #[test] diff --git a/rar-common/src/database/versionning.rs b/rar-common/src/database/versionning.rs index f46ec9f5..326be208 100644 --- a/rar-common/src/database/versionning.rs +++ b/rar-common/src/database/versionning.rs @@ -3,10 +3,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Debug; use super::migration::Migration; -use crate::version; -use crate::SettingsFile; - -use super::structs::*; +use crate::{FullSettingsFile, PACKAGE_VERSION}; #[derive(Deserialize, Serialize, Debug)] pub struct Versioning { @@ -18,7 +15,7 @@ pub struct Versioning { impl Versioning { pub fn new(data: T) -> Self { Self { - version: version::PACKAGE_VERSION.to_owned().parse().unwrap(), + version: PACKAGE_VERSION.to_owned().parse().unwrap(), data, } } @@ -27,12 +24,10 @@ impl Versioning { impl Default for Versioning { fn default() -> Self { Self { - version: version::PACKAGE_VERSION.to_owned().parse().unwrap(), + version: PACKAGE_VERSION.to_owned().parse().unwrap(), data: T::default(), } } } -pub(crate) const JSON_MIGRATIONS: &[Migration] = &[]; - -pub(crate) const SETTINGS_MIGRATIONS: &[Migration] = &[]; +pub(crate) const SETTINGS_MIGRATIONS: &[Migration] = &[]; diff --git a/rar-common/src/lib.rs b/rar-common/src/lib.rs index 68fe82c7..1e7073f0 100644 --- a/rar-common/src/lib.rs +++ b/rar-common/src/lib.rs @@ -47,60 +47,93 @@ // } // } -#[cfg(not(test))] -const ROOTASROLE: &str = "/etc/security/rootasrole.json"; -#[cfg(test)] -const ROOTASROLE: &str = "target/rootasrole.json"; +const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); -use std::{cell::RefCell, error::Error, ffi::OsStr, path::PathBuf, rc::Rc}; +use std::{ + cell::RefCell, + error::Error, + io::BufReader, + path::{Path, PathBuf}, + rc::Rc, +}; use bon::Builder; -use log::debug; +use libc::dev_t; +use log::{debug, warn}; +use nix::unistd::{getgroups, Group, Pid, Uid, User}; +use semver::Version; use serde::{Deserialize, Serialize}; -pub mod api; +//pub mod api; pub mod database; -pub mod plugin; +//pub mod plugin; pub mod util; -pub mod version; +use strum::EnumString; use util::{ dac_override_effective, open_with_privileges, read_effective, toggle_lock_config, - write_json_config, ImmutableLock, + write_cbor_config, write_json_config, ImmutableLock, }; use database::{ migration::Migration, structs::SConfig, - versionning::{Versioning, JSON_MIGRATIONS, SETTINGS_MIGRATIONS}, + versionning::{Versioning, SETTINGS_MIGRATIONS}, }; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Debug, Builder)] +pub struct Cred { + #[builder(field = getgroups().unwrap().iter().map(|gid| Group::from_gid(*gid).unwrap().unwrap()) + .collect())] + pub groups: Vec, + #[builder(field = User::from_uid(Uid::current()).unwrap().unwrap())] + pub user: User, + pub tty: Option, + #[builder(default = nix::unistd::getppid(), into)] + pub ppid: Pid, +} + +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Default, + Copy, + EnumString, + strum::VariantNames, +)] #[serde(rename_all = "lowercase")] +#[repr(u8)] pub enum StorageMethod { + #[default] + #[strum(ascii_case_insensitive)] JSON, + #[strum(ascii_case_insensitive)] + CBOR, // SQLite, // PostgreSQL, // MySQL, // LDAP, - #[serde(other)] - Unknown, } -pub enum Storage { - JSON(Rc>), +#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq, Default)] +pub struct SettingsFile { + pub storage: Settings, } -#[derive(Serialize, Deserialize, Debug, Clone, Builder)] -pub struct SettingsFile { +#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq, Default)] +pub struct FullSettingsFile { pub storage: Settings, #[serde(flatten)] - pub config: Rc>, + pub config: Option>>, } -#[derive(Serialize, Deserialize, Debug, Clone, Builder)] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq)] pub struct Settings { - #[builder(default = StorageMethod::JSON)] + #[builder(default = StorageMethod::JSON, into)] pub method: StorageMethod, #[serde(skip_serializing_if = "Option::is_none")] pub settings: Option, @@ -108,10 +141,10 @@ pub struct Settings { pub ldap: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default, PartialEq, Eq)] pub struct RemoteStorageSettings { #[serde(skip_serializing_if = "Option::is_none")] - #[builder(name = not_immutable,with = || false)] + #[builder(name = not_immutable,with = || env!("RAR_CFG_IMMUTABLE") == "true")] pub immutable: Option, #[serde(skip_serializing_if = "Option::is_none")] #[builder(into)] @@ -132,7 +165,7 @@ pub struct RemoteStorageSettings { pub properties: Option, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ConnectionAuth { pub user: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -141,7 +174,7 @@ pub struct ConnectionAuth { pub client_ssl: Option, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ClientSsl { pub enabled: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -152,13 +185,13 @@ pub struct ClientSsl { pub client_key: Option, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct Properties { pub use_unicode: bool, pub character_encoding: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct LdapSettings { pub enabled: bool, pub host: String, @@ -173,15 +206,6 @@ pub struct LdapSettings { pub group_filter: String, } -impl Default for SettingsFile { - fn default() -> Self { - Self { - storage: Settings::default(), - config: Rc::new(RefCell::new(SConfig::default())), - } - } -} - // Default implementation for Settings impl Default for Settings { fn default() -> Self { @@ -193,78 +217,554 @@ impl Default for Settings { } } -pub fn save_settings(settings: 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 +pub fn make_weak_config(config: &Rc>) { + for role in &config.as_ref().borrow().roles { + role.as_ref().borrow_mut()._config = Some(Rc::downgrade(config)); + for task in &role.as_ref().borrow().tasks { + task.as_ref().borrow_mut()._role = Some(Rc::downgrade(role)); + } + } +} + +pub fn full_save_settings( + path: &S, + settings: Rc>, + privileged: bool, +) -> Result<(), Box> +where + S: AsRef, +{ + Migration::migrate( + &Version::parse(PACKAGE_VERSION).unwrap(), + &mut *settings.as_ref().borrow_mut(), + SETTINGS_MIGRATIONS, + )?; + let immuable = settings + .as_ref() + .borrow() .storage .settings .as_ref() - .unwrap_or(&default_remote) - .path - .as_ref() - .unwrap_or(&into); - 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, ImmutableLock::Unset)?; + .unwrap_or(&RemoteStorageSettings::default()) + .immutable + .unwrap_or(env!("RAR_CFG_IMMUTABLE") == "true") + && privileged; + let separate = if let Some(rss) = &settings.as_ref().borrow().storage.settings { + let default_data_path = env!("RAR_CFG_DATA_PATH").to_string().into(); + let data_path = rss.path.as_ref().unwrap_or(&default_data_path); + if data_path != path.as_ref() { + Some(data_path.clone()) + } else { + None } + } else { + None + }; + + if let Some(data_path) = separate { + debug!("Saving settings in separate file"); + return separate_save(&path, &data_path, settings.clone(), immuable); } - debug!("Writing config file"); - let versionned: Versioning>> = Versioning::new(settings.clone()); - write_json_config(&versionned, ROOTASROLE)?; - if let Some(settings) = &settings.as_ref().borrow().storage.settings { - if settings.immutable.unwrap_or(true) { + + if immuable { + debug!("Toggling immutable off for config file"); + toggle_lock_config(path, ImmutableLock::Unset)?; + } + // a single file + let versionned: Versioning>> = Versioning::new(settings.clone()); + write_json_config(&versionned, path)?; + if immuable { + debug!("Toggling immutable on for config file"); + toggle_lock_config(path, ImmutableLock::Set)?; + } + Ok(()) +} + +fn separate_save( + settings_path: &S, + data_path: &T, + settings: Rc>, + immutable: bool, +) -> Result<(), Box> +where + S: AsRef, + T: AsRef, +{ + { + let storage_method = settings.as_ref().borrow().storage.method.clone(); + let binding = settings.as_ref().borrow_mut(); + let config = binding.config.as_ref().take().unwrap(); + let versioned_config: Versioning>> = Versioning::new(config.clone()); + if immutable { debug!("Toggling immutable off for config file"); - toggle_lock_config(path, ImmutableLock::Set)?; + toggle_lock_config(data_path, ImmutableLock::Unset)?; + } + debug!( + "Saving in {} : {}", + data_path.as_ref().display(), + serde_json::to_string_pretty(&versioned_config).unwrap() + ); + match storage_method { + StorageMethod::JSON => { + write_json_config(&versioned_config, data_path)?; + } + StorageMethod::CBOR => { + write_cbor_config(&versioned_config, data_path)?; + } + } + if immutable { + debug!("Toggling immutable on for config file"); + toggle_lock_config(data_path, ImmutableLock::Set)?; } } - debug!("Resetting dac privilege"); - dac_override_effective(false)?; + settings.as_ref().borrow_mut().config = None; + let versioned_settings: Versioning>> = + Versioning::new(settings.clone()); + if immutable { + debug!("Toggling immutable off for config file"); + toggle_lock_config(settings_path, ImmutableLock::Unset)?; + } + debug!( + "Saving in {} : {}", + settings_path.as_ref().display(), + serde_json::to_string_pretty(&versioned_settings).unwrap() + ); + write_json_config(&versioned_settings, settings_path)?; + if immutable { + debug!("Toggling immutable on for config file"); + toggle_lock_config(settings_path, ImmutableLock::Set)?; + } Ok(()) } -pub fn get_settings(path: &S) -> Result>, Box> +pub fn get_full_settings(path: &S) -> Result>, Box> where - S: AsRef + ?Sized, + S: AsRef, { // if file does not exist, return default settings if !std::path::Path::new(path.as_ref()).exists() { - return Ok(rc_refcell!(SettingsFile::default())); + return Ok(rc_refcell!(FullSettingsFile::default())); } // if user does not have read permission, try to enable privilege let file = open_with_privileges(path.as_ref())?; - let value: Versioning = serde_json::from_reader(file) + let value: Versioning = serde_json::from_reader(file) .inspect_err(|e| { debug!("Error reading file: {}", e); }) - .unwrap_or_default(); + .unwrap(); read_effective(false).or(dac_override_effective(false))?; - debug!("{}", serde_json::to_string_pretty(&value)?); let settingsfile = rc_refcell!(value.data); - if let Ok(true) = Migration::migrate( - &value.version, - &mut *settingsfile.as_ref().borrow_mut(), - SETTINGS_MIGRATIONS, - ) { - if let Ok(true) = Migration::migrate( - &value.version, - &mut *settingsfile - .as_ref() - .borrow_mut() - .config - .as_ref() - .borrow_mut(), - JSON_MIGRATIONS, - ) { - save_settings(settingsfile.clone())?; + debug!("settingsfile: {:?}", settingsfile); + let default_remote = RemoteStorageSettings::default(); + let into = env!("RAR_CFG_DATA_PATH").to_string().into(); + { + let mut binding = settingsfile.as_ref().borrow_mut(); + let data_path = binding + .storage + .settings + .as_ref() + .unwrap_or(&default_remote) + .path + .as_ref() + .unwrap_or(&into); + if data_path != path.as_ref() { + binding.config = Some(retrieve_sconfig(&binding.storage.method, data_path)?); } else { - debug!("No config migrations needed"); + make_weak_config(binding.config.as_ref().unwrap()); } - } else { - debug!("No settings migrations needed"); } - Ok(settingsfile) + + Ok(settingsfile.clone()) +} + +pub fn retrieve_sconfig( + file_type: &StorageMethod, + path: &PathBuf, +) -> Result>, Box> { + let file = open_with_privileges(path)?; + let value: Versioning>> = match file_type { + StorageMethod::JSON => serde_json::from_reader(file) + .inspect_err(|e| { + debug!("Error reading file: {}", e); + }) + .unwrap_or_default(), + StorageMethod::CBOR => cbor4ii::serde::from_reader(BufReader::new(file)) + .inspect_err(|e| { + debug!("Error reading file: {}", e); + }) + .unwrap_or_default(), + }; + make_weak_config(&value.data); + //read_effective(false).or(dac_override_effective(false))?; + //assert_eq!(value.version.to_string(), PACKAGE_VERSION, "Version mismatch"); + debug!("{}", serde_json::to_string_pretty(&value)?); + Ok(value.data) +} + +pub fn get_settings(path: &S) -> Result> +where + S: AsRef, +{ + // if user does not have read permission, try to enable privilege + let file = open_with_privileges(path.as_ref())?; + let value: Versioning = serde_json::from_reader(file) + .inspect_err(|e| { + debug!("Error reading file: {}", e); + }) + .unwrap_or_else(|_| { + warn!("Using default settings file!!"); + Default::default() + }); + //read_effective(false).or(dac_override_effective(false))?; + debug!("{}", serde_json::to_string_pretty(&value)?); + Ok(value.data) +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::io::Read; + + use crate::database::actor::SActor; + use crate::database::structs::{SCommand, SCommands, SCredentials, SRole, STask, SetBehavior}; + + use super::*; + + pub struct Defer(Option); + + impl Defer { + pub fn new(f: F) -> Self { + Defer(Some(f)) + } + } + + impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + + pub fn defer(f: F) -> Defer { + Defer::new(f) + } + + #[test] + fn test_get_settings_same_file() { + // Create a test JSON file + let value = "/tmp/test_get_settings_same_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(value).canonicalize().unwrap_or(value.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let config = Versioning::new(Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(value) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SConfig::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple( + "/usr/bin/true".to_string(), + )]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ))); + write_json_config(&config, value).unwrap(); + let settings = get_full_settings(&value).unwrap(); + assert_eq!(*config.data.borrow(), *settings.borrow()); + fs::remove_file(value).unwrap(); + } + + #[test] + fn test_get_settings_different_file() { + // Create a test JSON file + let external_file = "/tmp/test_get_settings_different_file_external.json"; + let test_file = "/tmp/test_get_settings_different_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or(test_file.into()); + if std::fs::remove_file(&test_file).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file) + .canonicalize() + .unwrap_or(external_file.into()); + if std::fs::remove_file(&external_file).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let settings_config = Versioning::new(Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(external_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SConfig::builder() + .role(SRole::builder("IGNORED").build()) + .build(), + ) + .build(), + ))); + write_json_config(&settings_config, test_file).unwrap(); + let config = SConfig::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + write_json_config(&Versioning::new(config.clone()), &external_file).unwrap(); + let settings = get_full_settings(&test_file).unwrap(); + assert_eq!( + *config.borrow(), + *settings.as_ref().borrow().config.as_ref().unwrap().borrow() + ); + fs::remove_file(test_file).unwrap(); + fs::remove_file(external_file).unwrap(); + } + + #[test] + fn test_save_settings_same_file() { + let test_file = "/tmp/test_save_settings_same_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or(test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + // Create a test JSON file + let config = Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(test_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SConfig::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple( + "/usr/bin/true".to_string(), + )]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + )); + full_save_settings(&test_file, config.clone(), false).unwrap(); + let settings = get_full_settings(&test_file).unwrap(); + assert_eq!(*config.borrow(), *settings.borrow()); + fs::remove_file(test_file).unwrap(); + } + + #[test] + fn test_save_settings_different_file() { + let external_file = "/tmp/test_save_settings_different_file_external.json"; + let test_file = "/tmp/test_save_settings_different_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or(test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file) + .canonicalize() + .unwrap_or(external_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let sconfig = SConfig::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + // Create a test JSON file + let config = Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(external_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config(sconfig.clone()) + .build(), + )); + full_save_settings(&test_file, config.clone(), false).unwrap(); + //assert that test_external.json contains /usr/bin/true + let mut file = open_with_privileges(external_file).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + assert!(content.contains("/usr/bin/true")); + + let mut file = open_with_privileges(test_file).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + assert!(!content.contains("/usr/bin/true")); + + let settings = get_full_settings(&test_file).unwrap(); + assert_eq!( + *sconfig.borrow(), + *settings.borrow().config.as_ref().unwrap().borrow() + ); + settings.as_ref().borrow_mut().config = None; + assert_eq!(*config.borrow(), *settings.borrow()); + fs::remove_file(test_file).unwrap(); + fs::remove_file(external_file).unwrap(); + } + + #[test] + fn test_save_cbor_format() { + let external_file = "/tmp/test_save_cbor_format.bin"; + let test_file = "/tmp/test_save_cbor_format.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or(test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file) + .canonicalize() + .unwrap_or(external_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let sconfig = SConfig::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + let settings = Rc::new(RefCell::new( + FullSettingsFile::builder() + .storage( + Settings::builder() + .method(StorageMethod::CBOR) + .settings( + RemoteStorageSettings::builder() + .path(external_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config(sconfig.clone()) + .build(), + )); + full_save_settings(&test_file, settings.clone(), false).unwrap(); + //asset that external_file is a binary file + let mut file = open_with_privileges(external_file).unwrap(); + // try to parse as ciborium + let mut content = Vec::new(); + file.read_to_end(&mut content).unwrap(); + let deserialized: Versioning>> = + cbor4ii::serde::from_reader(&content[..]).unwrap(); + assert_eq!(deserialized.version.to_string(), PACKAGE_VERSION); + fs::remove_file(test_file).unwrap(); + fs::remove_file(external_file).unwrap(); + } } diff --git a/rar-common/src/plugin/hashchecker.rs b/rar-common/src/plugin/hashchecker.rs index 3babc952..dde5e780 100644 --- a/rar-common/src/plugin/hashchecker.rs +++ b/rar-common/src/plugin/hashchecker.rs @@ -1,10 +1,10 @@ -use std::{fs::File, io::Read, os::fd::AsRawFd}; +use std::{error::Error, fs::File, io::Read, os::fd::AsRawFd}; use crate::{ api::PluginManager, database::structs::SCommand, open_with_privileges, - util::{final_path, parse_conf_command}, + util::{first_path, parse_conf_command}, }; use log::{debug, warn}; use nix::unistd::{access, AccessFlags}; @@ -77,34 +77,7 @@ fn complex_command_parse( debug!("Checking command {:?}", checker); match checker { Ok(checker) => { - let cmd = parse_conf_command(&checker.command)?; - let path = final_path(&cmd[0]); - if access(&path, AccessFlags::W_OK).is_ok() { - if checker.read_only.is_some_and(|read_only| read_only) { - return Err("Executor must not have write access to the executable".into()); - } - warn!("Executor has write access to the executable, this could lead to a race condition vulnerability"); - } - let mut open = open_with_privileges(&path)?; - if !is_immutable(&open)? && checker.immutable.is_some_and(|immutable| immutable) { - return Err("Executable file must be immutable".into()); - } - let mut buf = Vec::new(); - open.read_to_end(&mut buf)?; - let hash = compute(&checker.hash_type, &buf); - 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()) - } + process_hash_check(checker) } Err(e) => { debug!("Error parsing command {:?}", e); @@ -113,6 +86,45 @@ fn complex_command_parse( } } +fn process_hash_check(checker: HashChecker) -> Result, Box> { + let cmd = parse_conf_command(&checker.command)?; + let path = first_path(&cmd[0]).find( + |path| access(path, AccessFlags::W_OK).is_ok() + ); + if path.is_some() { + if checker.read_only.is_some_and(|read_only| read_only) { + return Err("Executor must not have write access to the executable".into()); + } + warn!("Executor has write access to the executable, this could lead to a race condition vulnerability"); + } + if let Some(path) = path { + let mut open = open_with_privileges(&path)?; + if !is_immutable(&open)? && checker.immutable.is_some_and(|immutable| immutable) { + return Err("Executable file must be immutable".into()); + } + let mut buf = Vec::new(); + open.read_to_end(&mut buf)?; + let hash = compute(&checker.hash_type, &buf); + 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()) + } + } else { + debug!("Path not found"); + Err("Path not found".into()) + } + +} + pub fn register() { PluginManager::subscribe_complex_command_parser(complex_command_parse) } @@ -128,7 +140,7 @@ mod tests { use super::*; use crate::database::actor::SActor; - use crate::database::finder::{Cred, TaskMatcher}; + //use crate::database::finder::{Cred, TaskMatcher}; use crate::{ database::structs::{IdTask, SCommand, SCommands, SConfig, SRole, STask}, rc_refcell, @@ -175,7 +187,7 @@ mod tests { let matching = config .matches(&cred, &None, &vec!["/tmp/hashchecker".to_string()]) .unwrap(); - assert!(matching.fully_matching()); + assert!(matching.score.fully_matching()); std::fs::remove_file("/tmp/hashchecker").unwrap(); } } diff --git a/rar-common/src/plugin/hierarchy.rs b/rar-common/src/plugin/hierarchy.rs index 91ffc4cc..7b6c1f71 100644 --- a/rar-common/src/plugin/hierarchy.rs +++ b/rar-common/src/plugin/hierarchy.rs @@ -29,7 +29,7 @@ fn find_in_parents( matcher: &mut TaskMatch, ) -> PluginResultAction { //precondition matcher user matches - if !matcher.user_matching() { + if !matcher.score.user_matching() { return PluginResultAction::Ignore; } let mut result = PluginResultAction::Ignore; @@ -43,8 +43,8 @@ fn find_in_parents( match role.as_ref().borrow().tasks.matches(user, filter, command) { Ok(matches) => { debug!("Parent role {} matched", parent); - if !matcher.command_matching() - || (matches.command_matching() + if !matcher.score.command_matching() + || (matches.score.command_matching() && matches.score.cmd_cmp(&matcher.score) == Ordering::Less) { debug!("Parent role {} is better", parent); diff --git a/rar-common/src/util.rs b/rar-common/src/util.rs index 8e24a667..f1a784c0 100644 --- a/rar-common/src/util.rs +++ b/rar-common/src/util.rs @@ -1,5 +1,4 @@ use std::{ - env, error::Error, fs::File, io, @@ -9,14 +8,14 @@ use std::{ use capctl::{prctl, CapState}; use capctl::{Cap, CapSet, ParseCapError}; + use libc::{FS_IOC_GETFLAGS, FS_IOC_SETFLAGS}; use log::{debug, warn}; use serde::Serialize; use strum::EnumIs; #[cfg(feature = "finder")] -use crate::api::PluginManager; -use crate::database::structs::SCommand; +use crate::database::score::CmdMin; pub const RST: &str = "\x1B[0m"; pub const BOLD: &str = "\x1B[1m"; @@ -186,73 +185,71 @@ fn remove_outer_quotes(input: &str) -> String { } } -pub fn parse_conf_command(command: &SCommand) -> Result, Box> { - match command { - SCommand::Simple(command) => parse_simple_command(command), - SCommand::Complex(command) => parse_complex_command(command), - } -} - -fn parse_simple_command(command: &str) -> Result, Box> { - shell_words::split(command).map_err(Into::into) -} - -fn parse_complex_command(command: &serde_json::Value) -> Result, Box> { - if let Some(array) = command.as_array() { - let result: Result, _> = array - .iter() - .map(|item| { - item.as_str() - .map(|s| s.to_string()) - .ok_or_else(|| "Invalid command".into()) - }) - .collect(); - result - } else { - parse_complex_command_with_finder(command) - } +pub fn all_paths_from_env>(env_path: &[&str], exe_name: P) -> Vec { + env_path + .iter() + .filter_map(|dir| { + let full_path = Path::new(dir).join(&exe_name); + debug!("Checking path: {:?}", full_path); + full_path.is_file().then_some(full_path) + }) + .collect() } #[cfg(feature = "finder")] -fn parse_complex_command_with_finder( - command: &serde_json::Value, -) -> Result, Box> { - let res = PluginManager::notify_complex_command_parser(command); - debug!("Parsed command {:?}", res); - res -} - -#[cfg(not(feature = "finder"))] -fn parse_complex_command_with_finder( - _command: &serde_json::Value, -) -> Result, Box> { - Err("Invalid command".into()) -} - -pub fn find_from_envpath>(exe_name: P) -> Option { - env::var_os("PATH").and_then(|paths| { - env::split_paths(&paths) - .filter_map(|dir| { - let full_path = dir.join(&exe_name); - if full_path.is_file() { - Some(full_path) - } else { - None - } - }) - .next() - }) +pub fn match_single_path(cmd_path: &PathBuf, role_path: &str) -> CmdMin { + use glob::Pattern; + if !role_path.ends_with(cmd_path.to_str().unwrap()) || !role_path.starts_with("/") { + // the files could not be the same + return CmdMin::empty(); + } + let mut match_status = CmdMin::empty(); + debug!("Matching path {:?} with {:?}", cmd_path, role_path); + if cmd_path == Path::new(role_path) { + match_status |= CmdMin::Match; + } else if let Ok(pattern) = Pattern::new(role_path) { + if pattern.matches_path(&cmd_path) { + match_status |= CmdMin::WildcardPath; + } + } + if match_status.is_empty() { + debug!( + "No match for path ``{:?}`` for evaluated path : ``{:?}``", + cmd_path, role_path + ); + } + match_status +} + +/* +pub fn all_paths_from_env>(exe_name: P) -> impl Iterator { + env::var_os("PATH") + .into_iter() + .flat_map(|path| { + env::split_paths(&path).collect::>() + }) + .filter_map(move |dir| { + let full_path = dir.join(&exe_name); + debug!("Checking path: {:?}", full_path); + if full_path.is_file() { + Some(full_path) + } else { + None + } + }) } -pub fn final_path(path: &str) -> PathBuf { - if let Some(env_path) = find_from_envpath(path) { - env_path - } else if let Ok(canon_path) = std::fs::canonicalize(path) { +pub fn first_path

(path: P) -> PathBuf +where + P: AsRef, { + if let Some(path) = all_paths_from_env(&path).next() { + path + }else if let Ok(canon_path) = std::fs::canonicalize(&path) { canon_path } else { - PathBuf::from(path) + path.as_ref().to_path_buf() } -} +}*/ #[cfg(debug_assertions)] pub fn subsribe(_: &str) -> Result<(), Box> { @@ -265,9 +262,8 @@ pub fn subsribe(_: &str) -> Result<(), Box> { #[cfg(not(debug_assertions))] pub fn subsribe(tool: &str) -> Result<(), Box> { - use env_logger::Env; use log::LevelFilter; - use syslog::{BasicLogger, Facility, Formatter3164}; + use syslog::Facility; syslog::init(Facility::LOG_AUTH, LevelFilter::Info, Some(tool))?; Ok(()) } @@ -325,6 +321,15 @@ where Ok(()) } +pub fn write_cbor_config(settings: &T, path: S) -> Result<(), Box> +where + S: std::convert::AsRef + Clone, +{ + let file = create_with_privileges(path)?; + cbor4ii::serde::to_writer(file, &settings)?; + Ok(()) +} + pub fn create_with_privileges>(p: P) -> Result { std::fs::File::create(&p).or_else(|e| { debug!( @@ -390,6 +395,26 @@ mod test { use super::*; + pub struct Defer(Option); + + impl Defer { + pub fn new(f: F) -> Self { + Defer(Some(f)) + } + } + + impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + + pub fn defer(f: F) -> Defer { + Defer::new(f) + } + #[test] fn test_remove_outer_quotes() { assert_eq!(remove_outer_quotes("'test'"), "test"); @@ -464,7 +489,11 @@ mod test { #[test] fn test_toggle_lock_config() { - let path = PathBuf::from("/tmp/test"); + let path = PathBuf::from("/tmp/rar_test_lock_config.lock"); + let _defer = defer(|| { + // Clean up the test file after the test is done + let _ = fs::remove_file(&path); + }); let file = File::create(&path).expect("Failed to create file"); let res = toggle_lock_config(&path, ImmutableLock::Set); let status = fs::read_to_string("/proc/self/status").unwrap(); diff --git a/rar-common/src/version.rs b/rar-common/src/version.rs deleted file mode 100644 index 270fc037..00000000 --- a/rar-common/src/version.rs +++ /dev/null @@ -1,4 +0,0 @@ -// This file is generated by build.rs -// Do not edit this file directly -// Instead edit build.rs and run cargo build -pub const PACKAGE_VERSION: &str = "3.0.6"; diff --git a/resources/arch/PKGBUILD b/resources/arch/PKGBUILD index 43b6acb3..e488694b 100644 --- a/resources/arch/PKGBUILD +++ b/resources/arch/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Eddie Billoir pkgname=rootasrole -pkgver=3.0.0_alpha.5 +pkgver=3.1.0 pkgrel=1 pkgdesc='Alternative to sudo to run some administrative commands that uses Linux capabilities and RBAC for scalability.' url='https://lechatp.github.io/RootAsRole/' diff --git a/resources/man/en_US.md b/resources/man/en_US.md index 74918798..29725c87 100644 --- a/resources/man/en_US.md +++ b/resources/man/en_US.md @@ -1,4 +1,4 @@ -% RootAsRole(8) RootAsRole 3.0.0 | System Manager's Manual +% RootAsRole(8) RootAsRole 3.1.0 | System Manager's Manual % Eddie Billoir % September 2024 diff --git a/resources/man/fr_FR.md b/resources/man/fr_FR.md index d358cbbf..61a71f6f 100644 --- a/resources/man/fr_FR.md +++ b/resources/man/fr_FR.md @@ -1,4 +1,4 @@ -% RootAsRole(8) RootAsRole 3.0.0 | Manuel de l'administrateur système +% RootAsRole(8) RootAsRole 3.1.0 | Manuel de l'administrateur système % % Septembre 2024 diff --git a/resources/rootasrole.json b/resources/rootasrole.json index 646f4c05..6bf274ed 100644 --- a/resources/rootasrole.json +++ b/resources/rootasrole.json @@ -1,5 +1,5 @@ { - "version": "3.0.5", + "version": "3.1.0", "storage": { "method": "json", "settings": { @@ -26,6 +26,7 @@ }, "env": { "default": "delete", + "override_behavior": false, "keep": [ "HOME", "USER", @@ -57,8 +58,10 @@ "PERL5LIB", "PERL5OPT", "PYTHONINSPECT" - ] + ], + "set": {} }, + "authentication": "perform", "root": "user", "bounding": "strict", "wildcard-denied": ";&|" diff --git a/src/chsr/cli/cli.pest b/src/chsr/cli/cli.pest index fd58dcc9..d63282c3 100644 --- a/src/chsr/cli/cli.pest +++ b/src/chsr/cli/cli.pest @@ -1,5 +1,5 @@ cli = { SOI ~ args ~ EOI } -args = { help | list | role | options_operations } +args = { help | convert_op | list | role | options_operations } list = { ("show" | "list" | "l") } set = { "set" | "s" } @@ -13,12 +13,24 @@ whitelist = { "whitelist" | "wl" } blacklist = { "blacklist" | "bl" } checklist = { "checklist" | "cl" } setlist = { "setlist" | "sl" } +convert = { "convert" | "c" } all = { "all" } name = @{ (!WHITESPACE ~ ANY)+ } +// ======================== +// convert +// ======================== +convert_op = { convert ~ convert_reconfigure? ~ convert_args } +convert_args = { from? ~ to } +from = { "--from" ~ convert_type ~ path } +to = { convert_type ~ path } +convert_type = _{ "json" | "cbor" } +convert_reconfigure = { "--reconfigure" | "-r" } + + // ======================== // role // ======================== diff --git a/src/chsr/cli/data.rs b/src/chsr/cli/data.rs index ab2b6235..51d549ce 100644 --- a/src/chsr/cli/data.rs +++ b/src/chsr/cli/data.rs @@ -1,17 +1,21 @@ -use std::collections::HashMap; +use std::{collections::HashMap, path::PathBuf}; +use bon::Builder; use capctl::CapSet; use chrono::Duration; use linked_hash_set::LinkedHashSet; use pest_derive::Parser; -use rar_common::database::{ - actor::{SActor, SGroups, SUserType}, - options::{ - EnvBehavior, EnvKey, OptType, PathBehavior, SAuthentication, SBounding, SPrivileged, - TimestampType, +use rar_common::{ + database::{ + actor::{SActor, SGroups, SUserType}, + options::{ + EnvBehavior, EnvKey, OptType, PathBehavior, SAuthentication, SBounding, SPrivileged, + TimestampType, + }, + structs::{IdTask, SetBehavior}, }, - structs::{IdTask, SetBehavior}, + StorageMethod, }; #[derive(Parser)] @@ -40,6 +44,7 @@ pub enum InputAction { Add, Del, Purge, + Convert, None, } @@ -89,6 +94,16 @@ pub struct Inputs { pub options_bounding: Option, pub options_wildcard: Option, pub options_auth: Option, + pub convertion: Option, + pub convert_reconfigure: bool, +} + +#[derive(Builder, Debug, Default)] +pub struct Convertion { + pub from_type: Option, + pub from: Option, + pub to_type: StorageMethod, + pub to: PathBuf, } impl Default for Inputs { @@ -122,6 +137,8 @@ impl Default for Inputs { options_bounding: None, options_wildcard: None, options_auth: None, + convertion: None, + convert_reconfigure: false, } } } diff --git a/src/chsr/cli/mod.rs b/src/chsr/cli/mod.rs index f83cd04b..0cb972d1 100644 --- a/src/chsr/cli/mod.rs +++ b/src/chsr/cli/mod.rs @@ -3,7 +3,7 @@ pub(crate) mod pair; pub(crate) mod process; pub(crate) mod usage; -use std::error::Error; +use std::{cell::RefCell, error::Error, rc::Rc}; use data::{Cli, Inputs, Rule}; @@ -11,12 +11,12 @@ use log::debug; use pair::recurse_pair; use pest::Parser; use process::process_input; +use rar_common::FullSettingsFile; use usage::print_usage; use crate::util::escape_parser_string_vec; -use rar_common::Storage; -pub fn main(storage: &Storage, args: I) -> Result> +pub fn main(storage: Rc>, args: I) -> Result> where I: IntoIterator, S: AsRef, @@ -34,7 +34,7 @@ where recurse_pair(pair, &mut inputs)?; } debug!("Inputs : {:?}", inputs); - process_input(storage, inputs) + process_input(&storage, inputs) } #[cfg(test)] @@ -47,13 +47,12 @@ mod tests { actor::SActor, actor::SGroups, options::*, - read_json_config, structs::{SCredentials, *}, versionning::Versioning, }, - get_settings, + get_full_settings, util::remove_with_privileges, - RemoteStorageSettings, Settings, SettingsFile, Storage, StorageMethod, + FullSettingsFile, RemoteStorageSettings, Settings, StorageMethod, }; use crate::ROOTASROLE; @@ -64,10 +63,30 @@ mod tests { use log::error; use test_log::test; - fn setup(name: &str) { + pub struct Defer(Option); + + impl Defer { + pub fn new(f: F) -> Self { + Defer(Some(f)) + } + } + + impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + + pub fn defer(f: F) -> Defer { + Defer::new(f) + } + + fn setup(name: &str) -> Defer { let file_path = format!("{}.{}", ROOTASROLE, name); let versionned = Versioning::new( - SettingsFile::builder() + FullSettingsFile::builder() .storage( Settings::builder() .method(StorageMethod::JSON) @@ -205,7 +224,7 @@ mod tests { .cred( SCredentials::builder() .setuid("user1") - .setgid(SGroupschooser::Group(SGroups::from([ + .setgid(SGroupschooser::Groups(SGroups::from([ "setgid1", "setgid2", ]))) .capabilities( @@ -236,13 +255,11 @@ mod tests { let jsonstr = serde_json::to_string_pretty(&versionned).unwrap(); file.write_all(jsonstr.as_bytes()).unwrap(); file.flush().unwrap(); + defer(move || { + remove_with_privileges(file_path).unwrap(); + }) } - fn teardown(name: &str) { - //Remove json test file - let path = format!("{}.{}", ROOTASROLE, name); - remove_with_privileges(path).unwrap(); - } // we need to test every commands // chsr r r1 create // chsr r r1 delete @@ -279,11 +296,10 @@ mod tests { #[test] fn test_all_main() { - setup("all_main"); + let _defer = setup("all_main"); let path = format!("{}.{}", ROOTASROLE, "all_main"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); - assert!(main(&Storage::JSON(config.clone()), vec!["--help"],) + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), vec!["--help"],) .inspect_err(|e| { error!("{}", e); }) @@ -291,131 +307,106 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), - "r r1 create".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), - "r complete delete".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - teardown("all_main"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r r1 create".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete delete".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); } #[test] fn test_r_complete_show_actors() { - setup("r_complete_show_actors"); + let _defer = setup("r_complete_show_actors"); let path = format!("{}.{}", ROOTASROLE, "r_complete_show_actors"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); - assert!(main( - &Storage::JSON(config.clone()), - "r complete show actors".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), - "r complete show tasks".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), - "r complete show all".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), - "r complete purge actors".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - teardown("r_complete_show_actors"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete show actors".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete show tasks".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete show all".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!( + main(settings.clone(), "r complete purge actors".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b) + ); } #[test] fn test_purge_tasks() { - setup("purge_tasks"); + let _defer = setup("purge_tasks"); let path = format!("{}.{}", ROOTASROLE, "purge_tasks"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); - assert!(main( - &Storage::JSON(config.clone()), - "r complete purge tasks".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - teardown("purge_tasks"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete purge tasks".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); } #[test] fn test_r_complete_purge_all() { - setup("r_complete_purge_all"); + let _defer = setup("r_complete_purge_all"); let path = format!("{}.{}", ROOTASROLE, "r_complete_purge_all"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); - assert!(main( - &Storage::JSON(config.clone()), - "r complete purge all".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); - teardown("r_complete_purge_all"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete purge all".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); } #[test] fn test_r_complete_grant_u_user1_g_group1_g_group2_group3() { - setup("r_complete_grant_u_user1_g_group1_g_group2_group3"); + let _defer = setup("r_complete_grant_u_user1_g_group1_g_group2_group3"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_grant_u_user1_g_group1_g_group2_group3" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete grant -u user1 -g group1 -g group2&group3".split(" "), ) .inspect_err(|e| { @@ -425,23 +416,44 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::user("user1").build())); - assert!(config.as_ref().borrow()[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::group("group1").build())); - assert!(config.as_ref().borrow()[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::group(["group2", "group3"]).build())); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete revoke -u user1 -g group1 -g group2&group3".split(" "), ) .inspect_err(|e| { @@ -451,31 +463,50 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::user("user1").build())); - assert!(!config.as_ref().borrow()[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::group("group1").build())); - assert!(!config.as_ref().borrow()[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] .as_ref() .borrow() .actors .contains(&SActor::group(["group2", "group3"]).build())); - teardown("r_complete_grant_u_user1_g_group1_g_group2_group3"); } #[test] fn test_r_complete_task_t_complete_show_all() { - setup("r_complete_task_t_complete_show_all"); + let _defer = setup("r_complete_task_t_complete_show_all"); let path = format!("{}.{}", ROOTASROLE, "r_complete_task_t_complete_show_all"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete task t_complete show all".split(" "), ) .inspect_err(|e| { @@ -485,8 +516,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), + settings.clone(), "r complete task t_complete show cmd".split(" "), ) .inspect_err(|e| { @@ -496,8 +528,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), + settings.clone(), "r complete task t_complete show cred".split(" "), ) .inspect_err(|e| { @@ -507,8 +540,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), + settings.clone(), "r complete task t_complete purge all".split(" "), ) .inspect_err(|e| { @@ -518,16 +552,14 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - teardown("r_complete_task_t_complete_show_all"); } #[test] fn test_r_complete_task_t_complete_purge_cmd() { - setup("r_complete_task_t_complete_purge_cmd"); + let _defer = setup("r_complete_task_t_complete_purge_cmd"); let path = format!("{}.{}", ROOTASROLE, "r_complete_task_t_complete_purge_cmd"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete task t_complete purge cmd".split(" "), ) .inspect_err(|e| { @@ -537,16 +569,14 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - teardown("r_complete_task_t_complete_purge_cmd"); } #[test] fn test_r_complete_task_t_complete_purge_cred() { - setup("r_complete_task_t_complete_purge_cred"); + let _defer = setup("r_complete_task_t_complete_purge_cred"); let path = format!("{}.{}", ROOTASROLE, "r_complete_task_t_complete_purge_cred"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete task t_complete purge cred".split(" "), ) .inspect_err(|e| { @@ -558,52 +588,76 @@ mod tests { .is_ok_and(|b| b)); debug!("====="); let path = format!("{}.{}", ROOTASROLE, "r_complete_task_t_complete_purge_cred"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); - let task_count = config.as_ref().borrow()[0].as_ref().borrow().tasks.len(); - assert!(main( - &Storage::JSON(config.clone()), - "r complete t t1 add".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + let task_count = settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks + .len(); + assert!(main(settings.clone(), "r complete t t1 add".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks.len(), + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks + .len(), task_count + 1 ); - assert!(main( - &Storage::JSON(config.clone()), - "r complete t t1 del".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| b)); + assert!(main(settings.clone(), "r complete t t1 del".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks.len(), + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks + .len(), task_count ); - teardown("r_complete_task_t_complete_purge_cred"); } #[test] fn test_r_complete_t_t_complete_cmd_setpolicy_deny_all() { - setup("r_complete_t_t_complete_cmd_setpolicy_deny_all"); + let _defer = setup("r_complete_t_t_complete_cmd_setpolicy_deny_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cmd_setpolicy_deny_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cmd setpolicy deny-all".split(" "), ) .inspect_err(|e| { @@ -614,26 +668,34 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .default_behavior, Some(SetBehavior::None) ); - teardown("r_complete_t_t_complete_cmd_setpolicy_deny_all"); } #[test] fn test_r_complete_t_t_complete_cmd_setpolicy_allow_all() { - setup("r_complete_t_t_complete_cmd_setpolicy_allow_all"); + let _defer = setup("r_complete_t_t_complete_cmd_setpolicy_allow_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cmd_setpolicy_allow_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cmd setpolicy allow-all".split(" "), ) .inspect_err(|e| { @@ -644,26 +706,34 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .default_behavior, Some(SetBehavior::All) ); - teardown("r_complete_t_t_complete_cmd_setpolicy_allow_all"); } #[test] fn test_r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces() { - setup("r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces"); + let _defer = setup("r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cmd whitelist add super command with spaces".split(" "), ) .inspect_err(|e| { @@ -673,14 +743,24 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .add .contains(&SCommand::Simple("super command with spaces".to_string()))); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cmd blacklist add super command with spaces".split(" "), ) .inspect_err(|e| { @@ -690,14 +770,24 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .sub .contains(&SCommand::Simple("super command with spaces".to_string()))); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cmd whitelist del super command with spaces".split(" "), ) .inspect_err(|e| { @@ -707,25 +797,33 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .add .contains(&SCommand::Simple("super command with spaces".to_string()))); - teardown("r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces"); } #[test] fn test_r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces() { - setup("r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces"); + let _defer = setup("r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), vec![ "r", "complete", @@ -747,22 +845,30 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .commands .sub .contains(&SCommand::Simple("super command with spaces".to_string()))); - teardown("r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces"); } #[test] fn test_r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2( ) { - setup("r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2"); + let _defer = setup("r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2"); let path = format!("{}.{}",ROOTASROLE,"r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); - assert!(main(&Storage::JSON(config.clone()), "r complete t t_complete cred set --caps cap_dac_override,cap_sys_admin,cap_sys_boot --setuid user1 --setgid group1,group2".split(" "), + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete t t_complete cred set --caps cap_dac_override,cap_sys_admin,cap_sys_boot --setuid user1 --setgid group1,group2".split(" "), ) .inspect_err(|e| { error!("{}", e); @@ -771,7 +877,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -780,7 +896,17 @@ mod tests { .unwrap() .default_behavior .is_none()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -789,7 +915,17 @@ mod tests { .unwrap() .add .has(Cap::DAC_OVERRIDE)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -798,7 +934,17 @@ mod tests { .unwrap() .add .has(Cap::SYS_ADMIN)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -808,7 +954,17 @@ mod tests { .add .has(Cap::SYS_BOOT)); assert!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -820,7 +976,17 @@ mod tests { == 0 ); assert!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -832,7 +998,7 @@ mod tests { == 3 ); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cred unset --caps cap_dac_override,cap_sys_admin,cap_sys_boot --setuid user1 --setgid group1,group2".split(" "), ) .inspect_err(|e| { @@ -842,7 +1008,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -851,31 +1027,49 @@ mod tests { .unwrap() .add .is_empty()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred .setuid .is_none()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred .setgid .is_none()); - teardown("r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2"); } #[test] fn test_r_complete_t_t_complete_cred_caps_setpolicy_deny_all() { - setup("r_complete_t_t_complete_cred_caps_setpolicy_deny_all"); + let _defer = setup("r_complete_t_t_complete_cred_caps_setpolicy_deny_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cred_caps_setpolicy_deny_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cred caps setpolicy deny-all".split(" "), ) .inspect_err(|e| { @@ -886,7 +1080,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -896,19 +1100,17 @@ mod tests { .default_behavior, SetBehavior::None ); - teardown("r_complete_t_t_complete_cred_caps_setpolicy_deny_all"); } #[test] fn test_r_complete_t_t_complete_cred_caps_setpolicy_allow_all() { - setup("r_complete_t_t_complete_cred_caps_setpolicy_allow_all"); + let _defer = setup("r_complete_t_t_complete_cred_caps_setpolicy_allow_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_cred_caps_setpolicy_allow_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cred caps setpolicy allow-all".split(" "), ) .inspect_err(|e| { @@ -919,7 +1121,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -929,16 +1141,14 @@ mod tests { .default_behavior, SetBehavior::All ); - teardown("r_complete_t_t_complete_cred_caps_setpolicy_allow_all"); } #[test] fn test_r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot( ) { - setup("r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); + let _defer = setup("r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); let path = format!("{}.{}",ROOTASROLE,"r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); - assert!(main(&Storage::JSON(config.clone()), "r complete t t_complete cred caps whitelist add cap_dac_override cap_sys_admin cap_sys_boot".split(" ")) + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete t t_complete cred caps whitelist add cap_dac_override cap_sys_admin cap_sys_boot".split(" ")) .inspect_err(|e| { error!("{}", e); }) @@ -946,7 +1156,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -955,7 +1175,17 @@ mod tests { .unwrap() .add .has(Cap::DAC_OVERRIDE)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -964,7 +1194,17 @@ mod tests { .unwrap() .add .has(Cap::SYS_ADMIN)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -973,16 +1213,14 @@ mod tests { .unwrap() .add .has(Cap::SYS_BOOT)); - teardown("r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); } #[test] fn test_r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot( ) { - setup("r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); + let _defer = setup("r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); let path = format!("{}.{}",ROOTASROLE,"r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); - assert!(main(&Storage::JSON(config.clone()), "r complete t t_complete cred caps blacklist add cap_dac_override cap_sys_admin cap_sys_boot".split(" "), + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete t t_complete cred caps blacklist add cap_dac_override cap_sys_admin cap_sys_boot".split(" "), ) .inspect_err(|e| { error!("{}", e); @@ -991,7 +1229,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1000,7 +1248,17 @@ mod tests { .unwrap() .sub .has(Cap::DAC_OVERRIDE)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1009,7 +1267,17 @@ mod tests { .unwrap() .sub .has(Cap::SYS_ADMIN)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1020,7 +1288,7 @@ mod tests { .has(Cap::SYS_BOOT)); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cred caps whitelist del cap_dac_override cap_sys_admin cap_sys_boot".split(" "), ) .inspect_err(|e| { @@ -1030,7 +1298,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1039,7 +1317,17 @@ mod tests { .unwrap() .add .has(Cap::DAC_OVERRIDE)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1048,7 +1336,17 @@ mod tests { .unwrap() .add .has(Cap::SYS_ADMIN)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1059,7 +1357,7 @@ mod tests { .has(Cap::SYS_BOOT)); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete cred caps blacklist del cap_dac_override cap_sys_admin cap_sys_boot".split(" "), ) .inspect_err(|e| { @@ -1069,7 +1367,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1078,7 +1386,17 @@ mod tests { .unwrap() .sub .has(Cap::DAC_OVERRIDE)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1087,7 +1405,17 @@ mod tests { .unwrap() .sub .has(Cap::SYS_ADMIN)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .cred @@ -1096,38 +1424,35 @@ mod tests { .unwrap() .sub .has(Cap::SYS_BOOT)); - teardown("r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot"); } #[test] fn test_options_show_all() { - setup("options_show_all"); + let _defer = setup("options_show_all"); let path = format!("{}.{}", ROOTASROLE, "options_show_all"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); - assert!(main( - &Storage::JSON(config.clone()), - "options show all".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), - "r complete options show path".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_ok_and(|b| !b)); - assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "options show all".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); + + assert!( + main(settings.clone(), "r complete options show path".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b) + ); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main( + settings.clone(), "r complete options show bounding".split(" "), ) .inspect_err(|e| { @@ -1137,19 +1462,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); - teardown("options_show_all"); } #[test] fn test_r_complete_t_t_complete_options_show_env() { - setup("r_complete_t_t_complete_options_show_env"); + let _defer = setup("r_complete_t_t_complete_options_show_env"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_options_show_env" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete options show env".split(" "), ) .inspect_err(|e| { @@ -1159,8 +1482,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), + settings.clone(), "r complete t t_complete options show root".split(" "), ) .inspect_err(|e| { @@ -1170,8 +1494,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), + settings.clone(), "r complete t t_complete options show bounding".split(" "), ) .inspect_err(|e| { @@ -1181,8 +1506,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), + settings.clone(), "r complete t t_complete options show wildcard-denied".split(" "), ) .inspect_err(|e| { @@ -1192,8 +1518,9 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| !b)); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), + settings.clone(), "r complete t t_complete o path set /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1203,19 +1530,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - teardown("r_complete_t_t_complete_options_show_env"); } #[test] fn test_r_complete_t_t_complete_o_path_setpolicy_delete_all() { - setup("r_complete_t_t_complete_o_path_setpolicy_delete_all"); + let _defer = setup("r_complete_t_t_complete_o_path_setpolicy_delete_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_path_setpolicy_delete_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path setpolicy delete-all".split(" "), ) .inspect_err(|e| { @@ -1225,7 +1550,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1238,19 +1573,17 @@ mod tests { .unwrap() .default_behavior .is_delete()); - teardown("r_complete_t_t_complete_o_path_setpolicy_delete_all"); } #[test] fn test_r_complete_t_t_complete_o_path_setpolicy_keep_unsafe() { - setup("r_complete_t_t_complete_o_path_setpolicy_keep_unsafe"); + let _defer = setup("r_complete_t_t_complete_o_path_setpolicy_keep_unsafe"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_path_setpolicy_keep_unsafe" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path setpolicy keep-unsafe".split(" "), ) .inspect_err(|e| { @@ -1260,7 +1593,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1274,7 +1617,7 @@ mod tests { .default_behavior .is_keep_unsafe()); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path setpolicy keep-safe".split(" "), ) .inspect_err(|e| { @@ -1284,7 +1627,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1299,7 +1652,7 @@ mod tests { .is_keep_safe()); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path setpolicy inherit".split(" "), ) .inspect_err(|e| { @@ -1309,7 +1662,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1322,19 +1685,17 @@ mod tests { .unwrap() .default_behavior .is_inherit()); - teardown("r_complete_t_t_complete_o_path_setpolicy_keep_unsafe"); } #[test] fn test_r_complete_t_t_complete_o_path_whitelist_add() { - setup("r_complete_t_t_complete_o_path_whitelist_add"); + let _defer = setup("r_complete_t_t_complete_o_path_whitelist_add"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_path_whitelist_add" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path whitelist add /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1345,7 +1706,17 @@ mod tests { }) .is_ok_and(|b| b)); let default = LinkedHashSet::new(); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1360,7 +1731,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/usr/bin".to_string())); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1376,7 +1757,7 @@ mod tests { .unwrap_or(&default) .contains(&"/bin".to_string())); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path whitelist del /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1386,7 +1767,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1401,7 +1792,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/usr/bin".to_string())); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1418,7 +1819,7 @@ mod tests { .contains(&"/bin".to_string())); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path whitelist purge".split(" "), ) .inspect_err(|e| { @@ -1428,7 +1829,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1445,7 +1856,7 @@ mod tests { .is_empty()); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path whitelist set /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1455,7 +1866,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1470,7 +1891,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/usr/bin".to_string())); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1486,7 +1917,17 @@ mod tests { .unwrap_or(&default) .contains(&"/bin".to_string())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1505,7 +1946,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path blacklist set /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1517,7 +1958,7 @@ mod tests { .is_ok_and(|b| b)); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path blacklist add /tmp".split(" "), ) .inspect_err(|e| { @@ -1527,7 +1968,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1543,7 +1994,7 @@ mod tests { .unwrap_or(&default) .contains(&"/tmp".to_string())); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path blacklist del /usr/bin:/bin".split(" "), ) .inspect_err(|e| { @@ -1555,7 +2006,17 @@ mod tests { .is_ok_and(|b| b)); debug!( "add : {:?}", - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1569,7 +2030,17 @@ mod tests { .sub ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1586,7 +2057,17 @@ mod tests { .len(), 1 ); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1601,7 +2082,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/tmp".to_string())); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1616,7 +2107,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/usr/bin".to_string())); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1631,19 +2132,17 @@ mod tests { .as_ref() .unwrap_or(&default) .contains(&"/bin".to_string())); - teardown("r_complete_t_t_complete_o_path_whitelist_add"); } #[test] fn test_r_complete_t_t_complete_o_path_blacklist_purge() { - setup("r_complete_t_t_complete_o_path_blacklist_purge"); + let _defer = setup("r_complete_t_t_complete_o_path_blacklist_purge"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_path_blacklist_purge" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o path blacklist purge".split(" "), ) .inspect_err(|e| { @@ -1653,19 +2152,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - teardown("r_complete_t_t_complete_o_path_blacklist_purge"); } #[test] fn test_r_complete_t_t_complete_o_env_keep_only_myvar_var2() { - setup("r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2"); + let _defer = setup("r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env keep-only MYVAR,VAR2".split(" "), ) .inspect_err(|e| { @@ -1675,7 +2172,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1688,7 +2195,17 @@ mod tests { .unwrap() .default_behavior .is_delete()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1703,7 +2220,17 @@ mod tests { .as_ref() .unwrap() .contains(&"MYVAR".to_string().into())); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1719,7 +2246,17 @@ mod tests { .unwrap() .contains(&"VAR2".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1736,19 +2273,17 @@ mod tests { .len(), 2 ); - teardown("r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2"); } #[test] fn test_r_complete_t_t_complete_o_env_delete_only_myvar_var2() { - setup("r_complete_t_t_complete_o_env_delete_only_MYVAR_VAR2"); + let _defer = setup("r_complete_t_t_complete_o_env_delete_only_MYVAR_VAR2"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_delete_only_MYVAR_VAR2" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env delete-only MYVAR,VAR2".split(" "), ) .inspect_err(|e| { @@ -1758,7 +2293,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1771,7 +2316,17 @@ mod tests { .unwrap() .default_behavior .is_keep()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1786,7 +2341,17 @@ mod tests { .as_ref() .unwrap() .contains(&"MYVAR".to_string().into())); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1802,7 +2367,17 @@ mod tests { .unwrap() .contains(&"VAR2".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1819,19 +2394,17 @@ mod tests { .len(), 2 ); - teardown("r_complete_t_t_complete_o_env_delete_only_MYVAR_VAR2"); } #[test] fn test_r_complete_t_t_complete_o_env_set_myvar_value_var2_value2() { - setup("r_complete_t_t_complete_o_env_set_MYVAR_value_VAR2_value2"); + let _defer = setup("r_complete_t_t_complete_o_env_set_MYVAR_value_VAR2_value2"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_set_MYVAR_value_VAR2_value2" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), r#"r complete t t_complete o env set MYVAR=value,VAR2="value2""#.split(" "), ) .inspect_err(|e| { @@ -1842,7 +2415,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1854,12 +2437,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("MYVAR") .unwrap(), (&"MYVAR".to_string(), &"value".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1871,12 +2466,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("VAR2") .unwrap(), (&"VAR2".to_string(), &"value2".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1888,22 +2495,22 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .len(), 2 ); - teardown("r_complete_t_t_complete_o_env_set_MYVAR_value_VAR2_value2"); } #[test] fn test_r_complete_t_t_complete_o_env_add_myvar_value_var2_value2() { - setup("r_complete_t_t_complete_o_env_add_MYVAR_value_VAR2_value2"); + let _defer = setup("r_complete_t_t_complete_o_env_add_MYVAR_value_VAR2_value2"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_add_MYVAR_value_VAR2_value2" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), r#"r complete t t_complete o env setlist set VAR3=value3"#.split(" "), ) .inspect_err(|e| { @@ -1914,7 +2521,7 @@ mod tests { }) .is_ok_and(|b| b)); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), r#"r complete t t_complete o env setlist add MYVAR=value,VAR2="value2""#.split(" "), ) .inspect_err(|e| { @@ -1925,7 +2532,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1937,12 +2554,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("MYVAR") .unwrap(), (&"MYVAR".to_string(), &"value".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1954,12 +2583,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("VAR2") .unwrap(), (&"VAR2".to_string(), &"value2".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1971,12 +2612,24 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("VAR3") .unwrap(), (&"VAR3".to_string(), &"value3".to_string()) ); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -1988,11 +2641,13 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .len(), 3 ); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), r#"r complete t t_complete o env setlist del MYVAR,VAR2"#.split(" "), ) .inspect_err(|e| { @@ -2003,7 +2658,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2015,10 +2680,22 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .len(), 1 ); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2030,9 +2707,21 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("MYVAR") .is_none()); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2044,10 +2733,12 @@ mod tests { .as_ref() .unwrap() .set + .as_ref() + .unwrap() .get_key_value("VAR2") .is_none()); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), r#"r complete t t_complete o env setlist purge"#.split(" "), ) .inspect_err(|e| { @@ -2057,7 +2748,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2069,20 +2770,18 @@ mod tests { .as_ref() .unwrap() .set - .is_empty()); - teardown("r_complete_t_t_complete_o_env_add_MYVAR_value_VAR2_value2"); + .is_none()); } #[test] fn test_r_complete_t_t_complete_o_env_setpolicy_delete_all() { - setup("r_complete_t_t_complete_o_env_setpolicy_delete_all"); + let _defer = setup("r_complete_t_t_complete_o_env_setpolicy_delete_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_setpolicy_delete_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env setpolicy delete-all".split(" "), ) .inspect_err(|e| { @@ -2093,7 +2792,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2107,19 +2816,17 @@ mod tests { .default_behavior, EnvBehavior::Delete ); - teardown("r_complete_t_t_complete_o_env_setpolicy_delete_all"); } #[test] fn test_r_complete_t_t_complete_o_env_setpolicy_keep_all() { - setup("r_complete_t_t_complete_o_env_setpolicy_keep_all"); + let _defer = setup("r_complete_t_t_complete_o_env_setpolicy_keep_all"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_setpolicy_keep_all" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env setpolicy keep-all".split(" "), ) .inspect_err(|e| { @@ -2130,7 +2837,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2144,19 +2861,17 @@ mod tests { .default_behavior, EnvBehavior::Keep ); - teardown("r_complete_t_t_complete_o_env_setpolicy_keep_all"); } #[test] fn test_r_complete_t_t_complete_o_env_setpolicy_inherit() { - setup("r_complete_t_t_complete_o_env_setpolicy_inherit"); + let _defer = setup("r_complete_t_t_complete_o_env_setpolicy_inherit"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_setpolicy_inherit" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env setpolicy inherit".split(" "), ) .inspect_err(|e| { @@ -2167,7 +2882,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2181,19 +2906,17 @@ mod tests { .default_behavior, EnvBehavior::Inherit ); - teardown("r_complete_t_t_complete_o_env_setpolicy_inherit"); } #[test] fn test_r_complete_t_t_complete_o_env_whitelist_add_myvar() { - setup("r_complete_t_t_complete_o_env_whitelist_add_MYVAR"); + let _defer = setup("r_complete_t_t_complete_o_env_whitelist_add_MYVAR"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_whitelist_add_MYVAR" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env whitelist add MYVAR".split(" "), ) .inspect_err(|e| { @@ -2203,7 +2926,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2219,7 +2952,17 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2237,7 +2980,7 @@ mod tests { > 1 ); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env whitelist del MYVAR".split(" "), ) .inspect_err(|e| { @@ -2247,7 +2990,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2264,7 +3017,7 @@ mod tests { .contains(&"MYVAR".to_string().into())); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env whitelist set MYVAR".split(" "), ) .inspect_err(|e| { @@ -2274,7 +3027,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2290,7 +3053,17 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2307,19 +3080,17 @@ mod tests { .len(), 1 ); - teardown("r_complete_t_t_complete_o_env_whitelist_add_MYVAR"); } #[test] fn test_r_complete_t_t_complete_o_env_whitelist_purge() { - setup("r_complete_t_t_complete_o_env_whitelist_purge"); + let _defer = setup("r_complete_t_t_complete_o_env_whitelist_purge"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_whitelist_purge" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env whitelist purge".split(" "), ) .inspect_err(|e| { @@ -2329,7 +3100,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2342,19 +3123,17 @@ mod tests { .unwrap() .keep .is_none()); - teardown("r_complete_t_t_complete_o_env_whitelist_purge"); } #[test] fn test_r_complete_t_t_complete_o_env_blacklist_add_myvar() { - setup("r_complete_t_t_complete_o_env_blacklist_add_MYVAR"); + let _defer = setup("r_complete_t_t_complete_o_env_blacklist_add_MYVAR"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_blacklist_add_MYVAR" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env blacklist add MYVAR".split(" "), ) .inspect_err(|e| { @@ -2364,7 +3143,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2380,7 +3169,7 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env blacklist del MYVAR".split(" "), ) .inspect_err(|e| { @@ -2390,7 +3179,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2405,19 +3204,17 @@ mod tests { .as_ref() .unwrap() .contains(&"MYVAR".to_string().into())); - teardown("r_complete_t_t_complete_o_env_blacklist_add_MYVAR"); } #[test] fn test_r_complete_t_t_complete_o_env_blacklist_set_myvar() { - setup("r_complete_t_t_complete_o_env_blacklist_set_MYVAR"); + let _defer = setup("r_complete_t_t_complete_o_env_blacklist_set_MYVAR"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_blacklist_set_MYVAR" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env blacklist set MYVAR".split(" "), ) .inspect_err(|e| { @@ -2427,7 +3224,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2443,7 +3250,17 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2460,19 +3277,17 @@ mod tests { .len(), 1 ); - teardown("r_complete_t_t_complete_o_env_blacklist_set_MYVAR"); } #[test] fn test_r_complete_t_t_complete_o_env_blacklist_purge() { - setup("r_complete_t_t_complete_o_env_blacklist_purge"); + let _defer = setup("r_complete_t_t_complete_o_env_blacklist_purge"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_blacklist_purge" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env blacklist purge".split(" "), ) .inspect_err(|e| { @@ -2482,7 +3297,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2495,19 +3320,17 @@ mod tests { .unwrap() .delete .is_none()); - teardown("r_complete_t_t_complete_o_env_blacklist_purge"); } #[test] fn test_r_complete_t_t_complete_o_env_checklist_add_myvar() { - setup("r_complete_t_t_complete_o_env_checklist_add_MYVAR"); + let _defer = setup("r_complete_t_t_complete_o_env_checklist_add_MYVAR"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_env_checklist_add_MYVAR" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env checklist add MYVAR".split(" "), ) .inspect_err(|e| { @@ -2517,7 +3340,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2534,7 +3367,7 @@ mod tests { .contains(&"MYVAR".to_string().into())); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env checklist del MYVAR".split(" "), ) .inspect_err(|e| { @@ -2544,7 +3377,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(!config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(!settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2561,7 +3404,7 @@ mod tests { .contains(&"MYVAR".to_string().into())); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env checklist set MYVAR".split(" "), ) .inspect_err(|e| { @@ -2571,7 +3414,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2587,7 +3440,17 @@ mod tests { .unwrap() .contains(&"MYVAR".to_string().into())); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2606,7 +3469,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o env checklist purge".split(" "), ) .inspect_err(|e| { @@ -2616,7 +3479,17 @@ mod tests { debug!("{}", e); }) .is_ok_and(|b| b)); - assert!(config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + assert!(settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2629,19 +3502,17 @@ mod tests { .unwrap() .check .is_none()); - teardown("r_complete_t_t_complete_o_env_checklist_add_MYVAR"); } #[test] fn test_r_complete_t_t_complete_o_root_privileged() { - setup("r_complete_t_t_complete_o_root_privileged"); + let _defer = setup("r_complete_t_t_complete_o_root_privileged"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_root_privileged" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o root privileged".split(" "), ) .inspect_err(|e| { @@ -2652,7 +3523,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2667,7 +3548,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o root user".split(" "), ) .inspect_err(|e| { @@ -2678,7 +3559,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2693,7 +3584,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o root inherit".split(" "), ) .inspect_err(|e| { @@ -2704,7 +3595,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2717,19 +3618,17 @@ mod tests { .unwrap(), &SPrivileged::Inherit ); - teardown("r_complete_t_t_complete_o_root_privileged"); } #[test] fn test_r_complete_t_t_complete_o_bounding_strict() { - setup("r_complete_t_t_complete_o_bounding_strict"); + let _defer = setup("r_complete_t_t_complete_o_bounding_strict"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_bounding_strict" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o bounding strict".split(" "), ) .inspect_err(|e| { @@ -2740,7 +3639,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2753,19 +3662,17 @@ mod tests { .unwrap(), &SBounding::Strict ); - teardown("r_complete_t_t_complete_o_bounding_strict"); } #[test] fn test_r_complete_t_t_complete_o_bounding_ignore() { - setup("r_complete_t_t_complete_o_bounding_ignore"); + let _defer = setup("r_complete_t_t_complete_o_bounding_ignore"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_bounding_ignore" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o bounding ignore".split(" "), ) .inspect_err(|e| { @@ -2776,7 +3683,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2789,19 +3706,17 @@ mod tests { .unwrap(), &SBounding::Ignore ); - teardown("r_complete_t_t_complete_o_bounding_ignore"); } #[test] fn test_r_complete_t_t_complete_o_bounding_inherit() { - setup("r_complete_t_t_complete_o_bounding_inherit"); + let _defer = setup("r_complete_t_t_complete_o_bounding_inherit"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_bounding_inherit" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o bounding inherit".split(" "), ) .inspect_err(|e| { @@ -2812,7 +3727,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2825,16 +3750,14 @@ mod tests { .unwrap(), &SBounding::Inherit ); - teardown("r_complete_t_t_complete_o_bounding_inherit"); } #[test] fn test_r_complete_t_t_complete_o_auth_skip() { - setup("r_complete_t_t_complete_o_auth_skip"); + let _defer = setup("r_complete_t_t_complete_o_auth_skip"); let path = format!("{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_auth_skip"); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o auth skip".split(" "), ) .inspect_err(|e| { @@ -2845,7 +3768,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2860,7 +3793,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o auth perform".split(" "), ) .inspect_err(|e| { @@ -2871,7 +3804,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2886,7 +3829,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o auth inherit".split(" "), ) .inspect_err(|e| { @@ -2897,7 +3840,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2910,19 +3863,17 @@ mod tests { .unwrap(), &SAuthentication::Inherit ); - teardown("r_complete_t_t_complete_o_auth_skip"); } #[test] fn test_r_complete_t_t_complete_o_wildcard_denied_set() { - setup("r_complete_t_t_complete_o_wildcard_denied_set"); + let _defer = setup("r_complete_t_t_complete_o_wildcard_denied_set"); let path = format!( "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_wildcard_denied_set" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o wildcard-denied set *".split(" "), ) .inspect_err(|e| { @@ -2933,7 +3884,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2948,7 +3909,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o wildcard-denied add ~".split(" "), ) .inspect_err(|e| { @@ -2959,7 +3920,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -2974,7 +3945,7 @@ mod tests { ); debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o wildcard-denied del *".split(" "), ) .inspect_err(|e| { @@ -2985,7 +3956,17 @@ mod tests { }) .is_ok_and(|b| b)); assert_eq!( - config.as_ref().borrow()[0].as_ref().borrow().tasks[0] + settings + .as_ref() + .borrow() + .config + .as_ref() + .unwrap() + .as_ref() + .borrow()[0] + .as_ref() + .borrow() + .tasks[0] .as_ref() .borrow() .options @@ -3003,10 +3984,9 @@ mod tests { "{}.{}", ROOTASROLE, "r_complete_t_t_complete_o_wildcard_denied_set" ); - let settings = get_settings(&path).expect("Failed to get settings"); - let config = read_json_config(settings.clone(), &path).expect("Failed to read json"); + let settings = get_full_settings(&path).expect("Failed to get settings"); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o timeout set --type uid --duration 15:05:10 --max-usage 7" .split(" "), ) @@ -3018,8 +3998,9 @@ mod tests { }) .is_ok_and(|b| b)); { - let binding = config.as_ref().borrow(); - let bindingrole = binding[0].as_ref().borrow(); + let bindingsettings = settings.as_ref().borrow(); + let bindingconfig = bindingsettings.config.as_ref().unwrap().as_ref().borrow(); + let bindingrole = bindingconfig[0].as_ref().borrow(); let bindingtask = bindingrole.tasks[0].as_ref().borrow(); let bindingopt = bindingtask.options.as_ref().unwrap().as_ref().borrow(); let timeout = bindingopt.timeout.as_ref().unwrap(); @@ -3029,7 +4010,7 @@ mod tests { } debug!("====="); assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o timeout unset --type --max-usage".split(" "), ) .inspect_err(|e| { @@ -3040,8 +4021,9 @@ mod tests { }) .is_ok_and(|b| b)); { - let binding = config.as_ref().borrow(); - let bindingrole = binding[0].as_ref().borrow(); + let bindingsettings = settings.as_ref().borrow(); + let bindingconfig = bindingsettings.config.as_ref().unwrap().as_ref().borrow(); + let bindingrole = bindingconfig[0].as_ref().borrow(); let bindingtask = bindingrole.tasks[0].as_ref().borrow(); let bindingopt = bindingtask.options.as_ref().unwrap().as_ref().borrow(); let timeout = bindingopt.timeout.as_ref().unwrap(); @@ -3049,7 +4031,7 @@ mod tests { assert_eq!(timeout.type_field, None); } assert!(main( - &Storage::JSON(config.clone()), + settings.clone(), "r complete t t_complete o timeout unset --type --duration --max-usage".split(" "), ) .inspect_err(|e| { @@ -3060,23 +4042,21 @@ mod tests { }) .is_ok_and(|b| b)); { - let binding = config.as_ref().borrow(); - let bindingrole = binding[0].as_ref().borrow(); + let bindingsettings = settings.as_ref().borrow(); + let bindingconfig = bindingsettings.config.as_ref().unwrap().as_ref().borrow(); + let bindingrole = bindingconfig[0].as_ref().borrow(); let bindingtask = bindingrole.tasks[0].as_ref().borrow(); let bindingopt = bindingtask.options.as_ref().unwrap().as_ref().borrow(); assert!(bindingopt.timeout.as_ref().is_none()); } - assert!(main( - &Storage::JSON(read_json_config(settings.clone(), &path).expect("Failed to read json")), - "r complete tosk".split(" "), - ) - .inspect_err(|e| { - error!("{}", e); - }) - .inspect(|e| { - debug!("{}", e); - }) - .is_err()); - teardown("r_complete_t_t_complete_o_wildcard_denied_set"); + let settings = get_full_settings(&path).expect("Failed to get settings"); + assert!(main(settings.clone(), "r complete tosk".split(" "),) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_err()); } } diff --git a/src/chsr/cli/pair.rs b/src/chsr/cli/pair.rs index 79f1655b..ed0d871b 100644 --- a/src/chsr/cli/pair.rs +++ b/src/chsr/cli/pair.rs @@ -5,14 +5,19 @@ use chrono::Duration; use linked_hash_set::LinkedHashSet; use log::{debug, warn}; use pest::iterators::Pair; +use strum::VariantNames; use crate::cli::data::{RoleType, TaskType}; -use rar_common::database::{ - actor::{SActor, SGroupType}, - options::{ - EnvBehavior, OptType, PathBehavior, SAuthentication, SBounding, SPrivileged, TimestampType, +use rar_common::{ + database::{ + actor::{SActor, SGroupType}, + options::{ + EnvBehavior, OptType, PathBehavior, SAuthentication, SBounding, SPrivileged, + TimestampType, + }, + structs::{IdTask, SetBehavior}, }, - structs::{IdTask, SetBehavior}, + StorageMethod, }; use super::data::*; @@ -67,6 +72,40 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box { inputs.setlist_type = Some(SetListType::Set); } + Rule::convert => { + inputs.action = InputAction::Convert; + } + Rule::to => { + let mut inner = pair.clone().into_inner(); + let temp_convertion = Default::default(); + let convertion = inputs.convertion.get_or_insert(temp_convertion); + convertion.to_type = inner.next().unwrap().as_str().parse().map_err(|e| { + warn!( + "Unknown type {}, types available : {}", + e, + StorageMethod::VARIANTS.join(", ") + ); + e + })?; + convertion.to = inner.next().unwrap().as_str().into(); + } + Rule::from => { + let mut inner = pair.clone().into_inner(); + let temp_convertion = Default::default(); + let convertion = inputs.convertion.get_or_insert(temp_convertion); + convertion.from_type = Some(inner.next().unwrap().as_str().parse().map_err(|e| { + warn!( + "Unknown type {}, types available : {}", + e, + StorageMethod::VARIANTS.join(", ") + ); + e + })?); + convertion.from = Some(inner.next().unwrap().as_str().into()); + } + Rule::convert_reconfigure => { + inputs.convert_reconfigure = true; + } // === setpolicies === Rule::cmd_policy => { inputs.action = InputAction::Set; @@ -443,7 +482,6 @@ mod test { fn get_inputs(args: &str) -> Inputs { let binding = make_args(args); - println!("{}", binding); let args = Cli::parse(Rule::cli, &binding); let args = match args { Ok(v) => v, diff --git a/src/chsr/cli/process.rs b/src/chsr/cli/process.rs index 4d5cc3c4..d427c061 100644 --- a/src/chsr/cli/process.rs +++ b/src/chsr/cli/process.rs @@ -1,3 +1,4 @@ +mod convert; mod json; use std::{cell::RefCell, error::Error, rc::Rc}; @@ -11,7 +12,7 @@ use rar_common::{ options::{Opt, OptType}, structs::{IdTask, RoleGetter}, }, - Storage, + FullSettingsFile, }; use super::{ @@ -19,7 +20,12 @@ use super::{ usage, }; -pub fn process_input(storage: &Storage, inputs: Inputs) -> Result> { +pub fn process_input( + storage: &Rc>, + inputs: Inputs, +) -> Result> { + let binding = storage.as_ref().borrow(); + let rconfig = binding.config.as_ref().unwrap(); match inputs { Inputs { action: InputAction::Help, @@ -34,29 +40,27 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => { - debug!("chsr list"); - match json::list_json( - rconfig, - role_id, - task_id, - options, - options_type, - task_type, - role_type, - ) { - Ok(_) => { - debug!("chsr list ok"); - Ok(false) - } - Err(e) => { - debug!("chsr list err {:?}", e); - Err(e) - } + } => { + debug!("chsr list"); + match json::list_json( + rconfig, + role_id, + task_id, + options, + options_type, + task_type, + role_type, + ) { + Ok(_) => { + debug!("chsr list ok"); + Ok(false) + } + Err(e) => { + debug!("chsr list err {:?}", e); + Err(e) } } - }, + } Inputs { // chsr role r1 add|del action, @@ -67,9 +71,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => role_add_del(rconfig, action, role_id, role_type), - }, + } => role_add_del(rconfig, action, role_id, role_type), + Inputs { // chsr role r1 grant|revoke -u u1 -u u2 -g g1,g2 action, @@ -77,9 +80,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => grant_revoke(rconfig, role_id, action, actors), - }, + } => grant_revoke(rconfig, role_id, action, actors), Inputs { // chsr role r1 task t1 add|del @@ -96,9 +97,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => task_add_del(rconfig, role_id, action, task_id, task_type), - }, + } => task_add_del(rconfig, role_id, action, task_id, task_type), + Inputs { //chsr role r1 task t1 cred set --caps "cap_net_raw,cap_sys_admin" action: InputAction::Set, @@ -112,16 +112,15 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => cred_set( - rconfig, - role_id, - task_id, - cred_caps, - cred_setuid, - cred_setgid, - ), - }, + } => cred_set( + rconfig, + role_id, + task_id, + cred_caps, + cred_setuid, + cred_setgid, + ), + Inputs { //chsr role r1 task t1 cred unset --caps "cap_net_raw,cap_sys_admin" action: InputAction::Del, @@ -135,16 +134,15 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => cred_unset( - rconfig, - role_id, - task_id, - cred_caps, - cred_setuid, - cred_setgid, - ), - }, + } => cred_unset( + rconfig, + role_id, + task_id, + cred_caps, + cred_setuid, + cred_setgid, + ), + Inputs { action, role_id: Some(role_id), @@ -154,20 +152,15 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => { - cred_caps(rconfig, role_id, task_id, setlist_type, action, pcred_caps) - } - }, + } => cred_caps(rconfig, role_id, task_id, setlist_type, action, pcred_caps), Inputs { role_id: Some(role_id), task_id: Some(task_id), cred_policy: Some(cred_policy), options: false, .. - } => match storage { - Storage::JSON(rconfig) => cred_setpolicy(rconfig, role_id, task_id, cred_policy), - }, + } => cred_setpolicy(rconfig, role_id, task_id, cred_policy), + Inputs { // chsr role r1 task t1 command whitelist add c1 action, @@ -176,19 +169,14 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => { - cmd_whitelist_action(rconfig, role_id, task_id, cmd_id, setlist_type, action) - } - }, + } => cmd_whitelist_action(rconfig, role_id, task_id, cmd_id, setlist_type, action), Inputs { role_id: Some(role_id), task_id: Some(task_id), cmd_policy: Some(cmd_policy), .. - } => match storage { - Storage::JSON(rconfig) => cmd_setpolicy(rconfig, role_id, task_id, cmd_policy), - }, + } => cmd_setpolicy(rconfig, role_id, task_id, cmd_policy), + // Set options Inputs { // chsr o env set A,B,C @@ -199,11 +187,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => { - env_set_policylist(rconfig, role_id, task_id, options_env, options_env_policy) - } - }, + } => env_set_policylist(rconfig, role_id, task_id, options_env, options_env_policy), Inputs { // chsr o root set privileged action: InputAction::Set, @@ -211,9 +195,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => set_privileged(rconfig, role_id, task_id, options_root), - }, + } => set_privileged(rconfig, role_id, task_id, options_root), + Inputs { // chsr o bounding set strict action: InputAction::Set, @@ -221,9 +204,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => set_bounding(rconfig, role_id, task_id, options_bounding), - }, + } => set_bounding(rconfig, role_id, task_id, options_bounding), + Inputs { // chsr o bounding set strict action: InputAction::Set, @@ -231,9 +213,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => set_authentication(rconfig, role_id, task_id, options_auth), - }, + } => set_authentication(rconfig, role_id, task_id, options_auth), + Inputs { // chsr o wildcard-denied set ";&*$" action, @@ -242,11 +223,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => { - json_wildcard(rconfig, role_id, task_id, action, options_wildcard) - } - }, + } => json_wildcard(rconfig, role_id, task_id, action, options_wildcard), Inputs { // chsr o path whitelist set a:b:c action: InputAction::Set, @@ -256,11 +233,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => { - path_set(rconfig, role_id, task_id, setlist_type, options_path) - } - }, + } => path_set(rconfig, role_id, task_id, setlist_type, options_path), Inputs { // chsr o path whitelist set a:b:c action: InputAction::Purge, @@ -270,9 +243,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => path_purge(rconfig, role_id, task_id, setlist_type), - }, + } => path_purge(rconfig, role_id, task_id, setlist_type), + Inputs { // chsr o env whitelist set A,B,C action: InputAction::Set, @@ -284,11 +256,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => { - env_whitelist_set(rconfig, role_id, task_id, setlist_type, options_env) - } - }, + } => env_whitelist_set(rconfig, role_id, task_id, setlist_type, options_env), Inputs { // chsr o timeout unset --type --duration --max-usage action: InputAction::Del, @@ -298,9 +266,8 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => unset_timeout(rconfig, role_id, task_id, timeout_arg), - }, + } => unset_timeout(rconfig, role_id, task_id, timeout_arg), + Inputs { // chsr o timeout set --type tty --duration 00:00:00 --max-usage 1 action: InputAction::Set, @@ -312,16 +279,14 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => set_timeout( - rconfig, - role_id, - task_id, - timeout_type, - timeout_duration, - timeout_max_usage, - ), - }, + } => set_timeout( + rconfig, + role_id, + task_id, + timeout_type, + timeout_duration, + timeout_max_usage, + ), Inputs { // chsr o path setpolicy delete-all @@ -331,11 +296,7 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => { - path_setpolicy(rconfig, role_id, task_id, options_path_policy) - } - }, + } => path_setpolicy(rconfig, role_id, task_id, options_path_policy), Inputs { // chsr o path whitelist add path1:path2:path3 action, @@ -347,17 +308,16 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => env_setlist_add( - rconfig, - role_id, - task_id, - setlist_type, - action, - options_key_env, - options_env_values, - ), - }, + } => env_setlist_add( + rconfig, + role_id, + task_id, + setlist_type, + action, + options_key_env, + options_env_values, + ), + Inputs { // chsr o path whitelist add path1:path2:path3 action, @@ -367,16 +327,15 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => path_setlist2( - rconfig, - role_id, - task_id, - setlist_type, - action, - options_path, - ), - }, + } => path_setlist2( + rconfig, + role_id, + task_id, + setlist_type, + action, + options_path, + ), + Inputs { // chsr o env setpolicy delete-all role_id, @@ -385,10 +344,14 @@ pub fn process_input(storage: &Storage, inputs: Inputs) -> Result match storage { - Storage::JSON(rconfig) => env_setpolicy(rconfig, role_id, task_id, options_env_policy), - }, + } => env_setpolicy(rconfig, role_id, task_id, options_env_policy), + Inputs { + action: InputAction::Convert, + convertion: Some(convertion), + convert_reconfigure, + .. + } => convert::convert(storage, convertion, convert_reconfigure), _ => Err("Unknown Input".into()), } } diff --git a/src/chsr/cli/process/convert.rs b/src/chsr/cli/process/convert.rs new file mode 100644 index 00000000..bd5b9fc7 --- /dev/null +++ b/src/chsr/cli/process/convert.rs @@ -0,0 +1,91 @@ +use std::{ + cell::RefCell, + error::Error, + fs::File, + io::{BufWriter, Write}, + path::Path, + rc::Rc, +}; + +use log::{debug, error}; +use rar_common::{retrieve_sconfig, FullSettingsFile, StorageMethod}; + +use crate::{cli::data::Convertion, ROOTASROLE}; + +pub fn convert( + settings: &Rc>, + convertion: Convertion, + convert_reconfigure: bool, +) -> Result> { + debug!("chsr convert"); + let mut settings = settings.borrow_mut(); + let default = Default::default(); + let default_path = Default::default(); + let path = settings + .storage + .settings + .as_ref() + .unwrap_or(&default) + .path + .as_ref() + .unwrap_or(&default_path); + let config = match convertion.from { + Some(ref from) => { + debug!("Convert from: {:?}", from); + let from_type = convertion.from_type.expect("Impossible state"); + if from == &convertion.to { + error!("The source and destination paths are the same"); + return Ok(false); + } + if from != path { + retrieve_sconfig(&from_type, from)? + } else { + settings + .config + .as_ref() + .expect("A configuration should be loaded") + .clone() + } + } + None => settings + .config + .clone() + .expect("A configuration should be loaded"), + }; + if !convert_reconfigure && convertion.to != *path { + write_config_file(&convertion, config) + } else if convert_reconfigure { + if convertion.to_type != StorageMethod::JSON && convertion.to == Path::new(ROOTASROLE) { + error!("The general settings file cannot be converted to another format than JSON\nThis file is used to determine the policy location and format. Please specify another path."); + return Ok(false); + } + settings.storage.method = convertion.to_type; + let mut remote = settings.storage.settings.clone().unwrap_or_default(); + remote.path = Some(convertion.to); + settings.storage.settings = Some(remote); + Ok(true) + } else { + error!("You are overwriting the current configuration file but you not specified the reconfigure (-r) option, this would break the current configuration"); + Ok(false) + } +} + +fn write_config_file( + convertion: &Convertion, + config: Rc>, +) -> Result> { + match convertion.to_type { + StorageMethod::JSON => { + let json = serde_json::to_string_pretty(&config)?; + let file = File::create(&convertion.to)?; + let mut writer = BufWriter::new(file); + writer.write_all(json.as_bytes())?; + } + StorageMethod::CBOR => { + let file = File::create(&convertion.to)?; + let writer = BufWriter::new(file); + cbor4ii::serde::to_writer(writer, &config)?; + } + } + Ok(false) +} diff --git a/src/chsr/cli/process/json.rs b/src/chsr/cli/process/json.rs index f7443870..81d3ec42 100644 --- a/src/chsr/cli/process/json.rs +++ b/src/chsr/cli/process/json.rs @@ -27,7 +27,6 @@ pub fn list_json( role_type: Option, ) -> Result<(), Box> { let config = rconfig.as_ref().borrow(); - debug!("list_json {:?}", config); if let Some(role_id) = role_id { if let Some(role) = rconfig.role(&role_id) { list_task(task_id, &role, options, options_type, task_type, role_type) @@ -51,6 +50,7 @@ fn list_task( if let Some(task_id) = task_id { if let Some(task) = role.as_ref().borrow().task(&task_id) { if options { + debug!("task {:?}", task); let rcopt = OptStack::from_task(task.clone()).to_opt(); let opt = rcopt.as_ref().borrow(); if let Some(opttype) = options_type { @@ -326,7 +326,7 @@ pub fn cred_set( } if let Some(setgid) = cred_setgid { task.as_ref().borrow_mut().cred.setgid = - Some(SGroupschooser::Group(setgid.clone())); + Some(SGroupschooser::Groups(setgid.clone())); } Ok(true) } @@ -1003,21 +1003,26 @@ pub fn env_setlist_add( Some(SetListType::Set) => match action { InputAction::Add => { debug!("options_env_values: {:?}", options_env_values); - env.set.extend(options_env_values.as_ref().unwrap().clone()); + env.set + .get_or_insert_default() + .extend(options_env_values.as_ref().unwrap().clone()); } InputAction::Del => { debug!("options_env_values: {:?}", options_env_values); options_key_env.as_ref().unwrap().into_iter().for_each(|k| { - env.set.remove(&k.to_string()); + if let Some(env) = &mut env.set { + env.remove(&k.to_string()); + } }); } InputAction::Purge => { debug!("options_env_values: {:?}", options_env_values); - env.set = HashMap::new(); + env.set = None; } InputAction::Set => { debug!("options_env_values: {:?}", options_env_values); - env.set = options_env_values.as_ref().unwrap().clone(); + env.set + .replace(options_env_values.as_ref().unwrap().clone()); } _ => unreachable!("Unknown action {:?}", action), }, diff --git a/src/chsr/cli/usage.rs b/src/chsr/cli/usage.rs index 31281a43..1815fc92 100644 --- a/src/chsr/cli/usage.rs +++ b/src/chsr/cli/usage.rs @@ -134,6 +134,17 @@ const RAR_USAGE_LISTING: &str = formatcp!( RST = RST ); +const RAR_CONVERT: &str = formatcp!( + "{UNDERLINE}{BOLD}Convert policy format :{RST} +chsr convert (-r) (--from [from_type] [from_file]) [to_type] [to_file] +Supported types: json, cbor + {BOLD}-r, --reconfigure{RST} Reconfigure /etc/security/rootasrole.json file to specify the new location. +{BOLD}Warning{RST}: the new location should be under a protected directory. + {BOLD}--from{RST} [from_type] [from_file] Specify the type and file to convert from.", + UNDERLINE = UNDERLINE, + BOLD = BOLD, + RST = RST); + pub fn help() -> Result> { debug!("chsr help"); println!("{}", LONG_ABOUT); @@ -176,6 +187,12 @@ fn rule_to_string(rule: &Rule) -> String { Rule::set => "set", Rule::setpolicy => "setpolicy", Rule::opt_env_listing => "whitelist, blacklist, checklist", + Rule::convert => "convert", + Rule::convert_type => "json or cbor", + Rule::convert_args => "--from, -r, --reconfigure or file_type", + Rule::convert_reconfigure => "-r or --reconfigure", + Rule::to => "[to_type] [to_file]", + Rule::from => "[from_type] [from_file]", _ => { println!("{:?}", rule); "unknown rule" @@ -199,6 +216,7 @@ pub fn print_usage(e: pest::error::Error) -> Result> RAR_USAGE_TASK, RAR_USAGE_CMD, RAR_USAGE_CRED, + RAR_CONVERT, ]); let e = e.clone().renamed_rules(|rule| { match rule { @@ -243,6 +261,9 @@ pub fn print_usage(e: pest::error::Error) -> Result> Rule::opt_env_listing => { usage = usage_concat(&[RAR_USAGE_OPTIONS_ENV, RAR_USAGE_LISTING]); } + Rule::convert => { + usage = usage_concat(&[RAR_CONVERT]); + } _ => {} }; rule_to_string(rule) diff --git a/src/chsr/main.rs b/src/chsr/main.rs index 04e22361..3e06db51 100644 --- a/src/chsr/main.rs +++ b/src/chsr/main.rs @@ -1,46 +1,26 @@ //extern crate sudoers_reader; -use log::{debug, error}; -use rar_common::{ - database::{read_json_config, save_json}, - plugin::register_plugins, - util::{drop_effective, read_effective, subsribe}, - Storage, -}; +use rar_common::util::{drop_effective, read_effective, subsribe}; mod cli; mod util; #[cfg(not(test))] -const ROOTASROLE: &str = "/etc/security/rootasrole.json"; +const ROOTASROLE: &str = env!("RAR_CFG_PATH"); #[cfg(test)] const ROOTASROLE: &str = "target/rootasrole.json"; #[cfg(not(tarpaulin_include))] fn main() -> Result<(), Box> { - use rar_common::{get_settings, StorageMethod}; + use rar_common::{full_save_settings, get_full_settings}; subsribe("chsr")?; drop_effective()?; - register_plugins(); - let settings = get_settings(ROOTASROLE).expect("Error on config read"); - let config = match settings.clone().as_ref().borrow().storage.method { - StorageMethod::JSON => Storage::JSON(read_json_config(settings.clone(), ROOTASROLE)?), - _ => { - error!("Unsupported storage method"); - std::process::exit(1); - } - }; + let settings = get_full_settings(&ROOTASROLE.to_string()).expect("Error on config read"); read_effective(false).expect("Operation not permitted"); - if cli::main(&config, std::env::args().skip(1)).is_ok_and(|b| b) { - match config { - Storage::JSON(config) => { - debug!("Saving configuration"); - save_json(settings, config)?; - Ok(()) - } - } + if cli::main(settings.clone(), std::env::args().skip(1)).is_ok_and(|b| b) { + full_save_settings(&ROOTASROLE.to_string(), settings, true) } else { Ok(()) } diff --git a/src/sr/finder/api/hashchecker.rs b/src/sr/finder/api/hashchecker.rs new file mode 100644 index 00000000..bbb03bbe --- /dev/null +++ b/src/sr/finder/api/hashchecker.rs @@ -0,0 +1,632 @@ +use std::{error::Error, fs::File, io::Read, os::fd::AsRawFd, path::PathBuf}; + +use ::serde::{Deserialize, Serialize}; +use libc::FS_IOC_GETFLAGS; +use log::{debug, warn}; +use nix::unistd::{access, AccessFlags}; +use rar_common::{ + database::score::CmdMin, + util::{all_paths_from_env, match_single_path, open_with_privileges}, +}; +use serde_json::to_value; +use sha2::Digest; + +use crate::finder::cmd::match_args; + +use super::{Api, ApiEvent, EventKey}; + +#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq)] +#[serde(rename_all = "lowercase", untagged)] +pub enum HashElement { + SHA224 { + #[serde(rename = "sha224")] + sha224: String, + }, + SHA256 { + #[serde(rename = "sha256")] + sha256: String, + }, + SHA384 { + #[serde(rename = "sha384")] + sha384: String, + }, + SHA512 { + #[serde(rename = "sha512")] + sha512: String, + }, +} + +#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq)] +struct HashChecker { + #[serde(flatten)] + hash: Option, + #[serde(alias = "read-only")] + read_only: Option, + immutable: Option, + command: String, +} + +fn new_complex_command(event: &mut ApiEvent) -> Result<(), Box> { + if let ApiEvent::ProcessComplexCommand( + value, + env_path, + cmd_path, + cmd_args, + cmd_min, + final_path, + ) = event + { + let hash_checker: HashChecker = serde_json::from_value(to_value(value)?)?; + process_hash_check( + hash_checker, + env_path, + cmd_path, + cmd_args, + *cmd_min, + *final_path, + ); + } + Ok(()) +} + +fn evaluate_hash(hashtype: &HashElement, hash: &[u8]) -> bool { + match hashtype { + HashElement::SHA224 { sha224 } => { + let mut hasher = sha2::Sha224::new(); + hasher.update(hash); + hasher.finalize().to_vec() == hex::decode(sha224).unwrap() + } + HashElement::SHA256 { sha256 } => { + let mut hasher = sha2::Sha256::new(); + hasher.update(hash); + hasher.finalize().to_vec() == hex::decode(sha256).unwrap() + } + HashElement::SHA384 { sha384 } => { + let mut hasher = sha2::Sha384::new(); + hasher.update(hash); + hasher.finalize().to_vec() == hex::decode(sha384).unwrap() + } + HashElement::SHA512 { sha512 } => { + let mut hasher = sha2::Sha512::new(); + hasher.update(hash); + hasher.finalize().to_vec() == hex::decode(sha512).unwrap() + } + } +} + +const FS_IMMUTABLE_FL: u32 = 0x00000010; + +fn is_immutable(file: &File) -> Result> { + let mut val = 0; + let fd = file.as_raw_fd(); + if unsafe { nix::libc::ioctl(fd, FS_IOC_GETFLAGS, &mut val) } < 0 { + debug!("Error getting flags {:?}", std::io::Error::last_os_error()); + return Err("Error getting flags".into()); + } + Ok(val & FS_IMMUTABLE_FL != 0) +} + +fn match_path( + checker: &HashChecker, + env_path: &[&str], + cmd_path: &PathBuf, + role_path: &String, + final_path: &mut Option, +) -> CmdMin { + if role_path == "**" { + return CmdMin::FullWildcardPath; + } else if cmd_path.is_absolute() { + let min = match_single_path(cmd_path, role_path); + verify_executable_conditions(checker, final_path, cmd_path, min).unwrap_or_default() + } else { + all_paths_from_env(env_path, cmd_path) + .iter() + .find_map(|cmd_path| { + let min = match_single_path(cmd_path, role_path); + verify_executable_conditions(checker, final_path, cmd_path, min) + }) + .unwrap_or_default() + } +} + +fn verify_executable_conditions( + checker: &HashChecker, + final_path: &mut Option, + cmd_path: &PathBuf, + min: CmdMin, +) -> Option { + if min.matching() { + if checker.read_only.is_some_and(|read_only| read_only) { + if access(cmd_path, AccessFlags::W_OK).is_ok() { + warn!("File should be read only but has write access"); + return None; + } + warn!("Executor has write access to the executable, this could lead to a race condition vulnerability"); + } + let open = open_with_privileges(cmd_path); + if open.is_err() { + return None; + } + let mut open = open.unwrap(); + if checker.immutable.is_some_and(|immutable| immutable) { + let is_immutable = is_immutable(&open); + if is_immutable.is_err() { + return None; + } + if !is_immutable.unwrap() { + warn!("File should be immutable but is not"); + return None; + } + } + if let Some(hash_element) = &checker.hash { + let mut buf = Vec::new(); + let res = open.read_to_end(&mut buf); + if res.is_err() { + warn!("Error reading file {:?}", res); + return None; + } + if !evaluate_hash(&hash_element, &buf) { + warn!("Hash does not match"); + return None; + } + } + *final_path = Some(cmd_path.clone()); + Some(min) + } else { + None + } +} + +/// Check if input command line is matching with role command line and return the score +fn match_command_line( + checker: &HashChecker, + env_path: &[&str], + cmd_path: &PathBuf, + cmd_args: &[String], + role_command: &[String], + cmd_min: &mut CmdMin, + final_path: &mut Option, +) { + let mut result = match_path(checker, env_path, cmd_path, &role_command[0], final_path); + if result.is_empty() || role_command.len() == 1 { + *cmd_min = result; + return; + } + match match_args(cmd_args, &shell_words::join(&role_command[1..])) { + Ok(args_result) => result |= args_result, + Err(err) => { + debug!("Error: {}", err); + return; + } + } + *cmd_min = result; +} + +fn process_hash_check( + checker: HashChecker, + env_path: &[&str], + cmd_path: &PathBuf, + cmd_args: &[String], + min_score: &mut CmdMin, + final_path: &mut Option, +) { + match shell_words::split(&checker.command) + .map_err(|e| Into::>::into(e)) + { + Ok(command) => { + match_command_line( + &checker, env_path, cmd_path, cmd_args, &command, min_score, final_path, + ); + } + Err(err) => { + warn!("Error: {}", err); + } + } +} + +pub fn register() { + Api::register(EventKey::NewComplexCommand, new_complex_command); +} + +#[cfg(test)] +mod tests { + use std::{ + fs::File, + io::Read, + path::{Path, PathBuf}, + }; + + use capctl::{CapSet, CapState}; + use log::debug; + use nix::sys::stat::{fchmodat, Mode}; + use rar_common::{ + database::score::CmdMin, + util::{immutable_effective, toggle_lock_config}, + }; + use serde::de::DeserializeSeed; + use sha2::{Digest, Sha224, Sha256, Sha384, Sha512}; + + use crate::finder::{api::hashchecker::register, de::DCommandDeserializer}; + pub struct Defer(Option); + + impl Defer { + pub fn new(f: F) -> Self { + Defer(Some(f)) + } + } + + impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + + pub fn defer(f: F) -> Defer { + Defer::new(f) + } + + fn set_read_only(path: &Path) -> nix::Result<()> { + // Set permissions to read-only for owner, group, and others + fchmodat( + None, // Relative to the current directory + path, + Mode::S_IRUSR // Owner read + | Mode::S_IRGRP // Group read + | Mode::S_IROTH, // Others read + nix::sys::stat::FchmodatFlags::NoFollowSymlink, // No special flags + )?; + Ok(()) + } + + #[test] + fn test_dcommand_seed_hashchecker() { + register(); + let filename = "test.sh"; + let _cleanup = defer(|| { + let filename = PathBuf::from(filename) + .canonicalize() + .unwrap_or(filename.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + //create the file + File::create(&filename).unwrap(); + let filename = PathBuf::from(filename).canonicalize().unwrap(); + //call sha256sum on the file + + let mut file = File::open(&filename).unwrap(); + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).unwrap(); + + let mut sha224hasher = Sha224::new(); + sha224hasher.update(&buffer); + let sha224 = sha224hasher.finalize(); + let json = format!( + r#"{{"sha224": "{:x}", "command": "{} -l"}}"#, + sha224, + &filename.display() + ); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + assert_eq!(final_path, Some(PathBuf::from(&filename))); + assert_eq!(cmd_min, CmdMin::Match); + + let mut sha256hasher = Sha256::new(); + sha256hasher.update(&buffer); + let sha256 = sha256hasher.finalize(); + + let json = format!( + r#"{{"sha256": "{:x}", "command": "{} -l"}}"#, + sha256, + &filename.display() + ); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + assert_eq!(final_path, Some(PathBuf::from(&filename))); + assert_eq!(cmd_min, CmdMin::Match); + + let mut sha384hasher = Sha384::new(); + sha384hasher.update(&buffer); + let sha384 = sha384hasher.finalize(); + + let json = format!( + r#"{{"sha384": "{:x}", "command": "{} -l"}}"#, + sha384, + &filename.display() + ); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + assert_eq!(final_path, Some(PathBuf::from(&filename))); + assert_eq!(cmd_min, CmdMin::Match); + + let mut sha512hasher = Sha512::new(); + sha512hasher.update(&buffer); + let sha512 = sha512hasher.finalize(); + let json = format!( + r#"{{"sha512": "{:x}", "command": "{} -l"}}"#, + sha512, + &filename.display() + ); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + assert_eq!(final_path, Some(PathBuf::from(&filename))); + assert_eq!(cmd_min, CmdMin::Match); + } + + #[test] + fn test_read_only_immutable() { + register(); + // remove root privileges + let current = CapState::get_current(); + let mut current = current.unwrap(); + current.effective = CapSet::empty(); + current.permitted = CapSet::empty(); + current.inheritable = CapSet::empty(); + current.set_current().unwrap(); + let filename = "/tmp/test_ro.sh"; + let _cleanup = defer(|| { + let filename = PathBuf::from(filename) + .canonicalize() + .unwrap_or(filename.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + //create the file + File::create(&filename).unwrap(); + + let filename = PathBuf::from(filename).canonicalize().unwrap(); + //call sha256sum on the file + + let json = format!( + r#"{{"read-only": true, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let json = format!( + r#"{{"read-only": true, "immutable": false, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let json = format!( + r#"{{"read-only": false, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let json = format!( + r#"{{"read-only": false, "immutable": false, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, PathBuf::from(&filename).canonicalize().ok()); + assert_eq!(cmd_min, CmdMin::Match); + + set_read_only(filename.as_path()).unwrap(); + + let json = format!( + r#"{{"read-only": true, "immutable": false, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, PathBuf::from(&filename).canonicalize().ok()); + assert_eq!(cmd_min, CmdMin::Match); + + let json = format!( + r#"{{"read-only": true, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let json = format!( + r#"{{"read-only": true, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + + let mut immutable = false; + if immutable_effective(true).is_ok() { + toggle_lock_config(&filename, rar_common::util::ImmutableLock::Unset).unwrap(); + immutable = true; + immutable_effective(false).unwrap(); + } + let json = format!( + r#"{{"read-only": true, "immutable": {}, "command": "{}"}}"#, + immutable, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + assert_eq!(final_path, PathBuf::from(&filename).canonicalize().ok()); + assert_eq!(cmd_min, CmdMin::Match); + + let json = format!( + r#"{{"read-only": true, "immutable": true, "command": "{}"}}"#, + &filename.display() + ); + debug!("json: {}", json); + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from(&filename).canonicalize().unwrap(), + cmd_args: &vec!["-l".to_string()], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + if let Err(e) = &result { + debug!("Error: {}", e); + } + assert!(result.is_ok()); + if immutable { + assert_eq!(final_path, PathBuf::from(&filename).canonicalize().ok()); + assert_eq!(cmd_min, CmdMin::Match); + } else { + assert_eq!(final_path, None); + assert_eq!(cmd_min, CmdMin::empty()); + } + } +} diff --git a/src/sr/finder/api/hierarchy.rs b/src/sr/finder/api/hierarchy.rs new file mode 100644 index 00000000..a89d06ae --- /dev/null +++ b/src/sr/finder/api/hierarchy.rs @@ -0,0 +1,69 @@ +use std::error::Error; + +use bon::builder; +use serde_json_borrow::Value; + +use crate::{ + finder::{de::DLinkedRole, options::BorrowedOptStack, BestExecSettings}, + Cli, +}; + +/// This module is not thread-safe. +use super::{Api, ApiEvent, EventKey}; + +fn find_in_parents(event: &mut ApiEvent) -> Result<(), Box> { + if let ApiEvent::BestRoleSettingsFound(cli, role, opt_stack, env_path, settings, matching) = + event + { + return match role.role()._extra_values.get("parents") { + Some(Value::Array(parents)) => { + let mut parents = parents.iter(); + while let Some(Value::Str(parent)) = parents.next() { + evaluate_parent_role() + .parent(parent.as_ref()) + .cli(cli) + .role(role) + .opt_stack(opt_stack) + .settings(settings) + .matching(matching) + .env_path(&env_path) + .call()?; + } + Ok(()) + } + Some(Value::Str(parent)) => evaluate_parent_role() + .parent(parent.as_ref()) + .cli(cli) + .role(role) + .opt_stack(opt_stack) + .settings(settings) + .matching(matching) + .env_path(&env_path) + .call(), + Some(_) => Err("Invalid parent value".into()), + None => Ok(()), + }; + } + Ok(()) +} + +#[builder] +fn evaluate_parent_role<'a>( + parent: &str, + cli: &mut &Cli, + role: &mut &DLinkedRole<'_, 'a>, + opt_stack: &mut BorrowedOptStack<'a>, + settings: &mut &mut BestExecSettings, + matching: &mut &mut bool, + env_path: &[&str], +) -> Result<(), Box> { + Ok(if let Some(role) = role.config().role(parent) { + for task in role.tasks() { + **matching |= settings.task_settings(cli, &task, opt_stack, env_path)?; + } + }) +} + +pub fn register() { + Api::register(EventKey::BestRoleSettings, find_in_parents); +} diff --git a/src/sr/finder/api/mod.rs b/src/sr/finder/api/mod.rs new file mode 100644 index 00000000..ada4a059 --- /dev/null +++ b/src/sr/finder/api/mod.rs @@ -0,0 +1,125 @@ +use std::{cell::UnsafeCell, collections::HashMap, error::Error, path::PathBuf}; + +use once_cell::sync::Lazy; +use rar_common::database::score::{CmdMin, Score}; +use serde_json_borrow::Value; +use strum::Display; + +use crate::Cli; + +use super::{ + de::{DConfigFinder, DLinkedRole, DLinkedTask}, + options::BorrowedOptStack, + BestExecSettings, +}; + +mod hashchecker; +mod hierarchy; +mod ssd; + +thread_local! { + static API: Lazy> = Lazy::new(|| UnsafeCell::new(Api::new())); +} + +pub struct Api { + callbacks: + HashMap Result<(), Box> + Send>>>, +} + +#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, Display)] +pub enum EventKey { + BestGlobalSettings, + BestRoleSettings, + BestTaskSettings, + NewComplexCommand, + ActorMatching, +} + +#[allow(dead_code)] +pub enum ApiEvent<'a, 't, 'c, 'f, 'g, 'h, 'i, 'j, 'k> { + BestGlobalSettingsFound( + &'f Cli, + &'g DConfigFinder<'a>, + &'j mut BorrowedOptStack<'a>, + &'h mut BestExecSettings, + &'i mut bool, + ), + BestRoleSettingsFound( + &'f Cli, + &'g DLinkedRole<'c, 'a>, + &'h mut BorrowedOptStack<'a>, + &'k &'k [&'k str], + &'i mut BestExecSettings, + &'j mut bool, + ), + BestTaskSettingsFound( + &'f Cli, + &'g DLinkedTask<'t, 'c, 'a>, + &'j mut BorrowedOptStack<'a>, + &'h mut BestExecSettings, + &'i mut Score, + ), + // NewComplexCommand (Value, env_path, cmd_path, cmd_args, cmd_min, final_path), + ProcessComplexCommand( + &'f Value<'a>, + &'g [&'g str], + &'h PathBuf, + &'i [String], + &'j mut CmdMin, + &'k mut Option, + ), + ActorMatching( + &'f DLinkedRole<'c, 'a>, + &'g mut BestExecSettings, + &'h mut bool, + ), +} + +impl ApiEvent<'_, '_, '_, '_, '_, '_, '_, '_, '_> { + fn get_key(&self) -> EventKey { + match self { + ApiEvent::BestGlobalSettingsFound(..) => EventKey::BestGlobalSettings, + ApiEvent::BestRoleSettingsFound(..) => EventKey::BestRoleSettings, + ApiEvent::BestTaskSettingsFound(..) => EventKey::BestTaskSettings, + ApiEvent::ProcessComplexCommand(..) => EventKey::NewComplexCommand, + ApiEvent::ActorMatching(..) => EventKey::ActorMatching, + } + } +} + +impl Api { + fn new() -> Self { + Api { + callbacks: HashMap::new(), + } + } + pub fn notify(mut event: ApiEvent) -> Result<(), Box> { + let key = event.get_key(); + API.with(|api| -> Result<(), Box> { + let api = unsafe { &mut *api.get() }; + if let Some(callbacks) = api.callbacks.get(&key) { + for callback in callbacks.iter() { + callback(&mut event)?; + } + } + Ok(()) + })?; + Ok(()) + } + pub fn register(event: EventKey, function: F) + where + F: Fn(&mut ApiEvent) -> Result<(), Box> + Send + 'static, + { + API.with(|api| unsafe { + let api = &mut *api.get(); + let callbacks = api.callbacks.entry(event).or_insert_with(Vec::new); + callbacks.push(Box::new(function)); + }); + } +} + +pub(super) fn register_plugins() { + ssd::register(); + hashchecker::register(); + hierarchy::register(); +} diff --git a/src/sr/finder/api/ssd.rs b/src/sr/finder/api/ssd.rs new file mode 100644 index 00000000..a76476aa --- /dev/null +++ b/src/sr/finder/api/ssd.rs @@ -0,0 +1,76 @@ +use std::{ + collections::HashSet, + error::Error, + hash::{DefaultHasher, Hash, Hasher}, +}; + +use log::error; +use serde_json_borrow::Value; + +use crate::finder::de::DLinkedRole; + +use super::{Api, ApiEvent}; + +fn calculate_hash(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + +fn check_ssd_recursive( + role: &DLinkedRole, + visited: &mut HashSet, +) -> Result> { + if let Some(Value::Array(ssd)) = role.role()._extra_values.get("ssd") { + for ssd in ssd.iter() { + if let Value::Str(ssd) = ssd { + if visited.contains(&calculate_hash(ssd)) { + continue; // Avoid infinite recursion + } + visited.insert(calculate_hash(ssd)); + if let Some(r) = role.config().role(ssd) { + if r.role().user_min.matching() { + return Ok(true); + } + if check_ssd_recursive(&r, visited)? { + return Ok(true); + } + } + } + } + } else if let Some(Value::Str(ssd)) = role.role()._extra_values.get("ssd") { + if visited.contains(&calculate_hash(ssd)) { + return Ok(false); // Avoid infinite recursion + } + visited.insert(calculate_hash(ssd)); + if let Some(r) = role.config().role(ssd) { + if r.role().user_min.matching() { + return Ok(true); + } + if check_ssd_recursive(&r, visited)? { + return Ok(true); + } + } + } else if let Some(_) = role.role()._extra_values.get("ssd") { + error!("Invalid SSD value"); + return Err("Invalid SSD value".into()); + } + Ok(false) +} + +fn check_ssd(event: &mut ApiEvent) -> Result<(), Box> { + if let ApiEvent::ActorMatching(role, _settings, matching) = event { + if role.role().user_min.matching() { + let mut visited: HashSet = HashSet::new(); + if check_ssd_recursive(role, &mut visited)? { + **matching = false; + return Ok(()); + } + } + } + Ok(()) +} + +pub fn register() { + Api::register(super::EventKey::ActorMatching, check_ssd); +} diff --git a/src/sr/finder/cmd.rs b/src/sr/finder/cmd.rs new file mode 100644 index 00000000..d5e747c7 --- /dev/null +++ b/src/sr/finder/cmd.rs @@ -0,0 +1,534 @@ +use log::{debug, warn}; +use rar_common::{ + database::score::CmdMin, + util::{all_paths_from_env, match_single_path}, +}; +use std::path::PathBuf; + +fn match_path( + env_path: &[&str], + cmd_path: &PathBuf, + role_path: &String, + previous_min: &CmdMin, + final_path: &mut Option, +) -> CmdMin { + if role_path == "**" { + return CmdMin::FullWildcardPath; + } else if cmd_path.is_absolute() { + let min = match_single_path(cmd_path, role_path); + if min.better(&previous_min) { + *final_path = Some(cmd_path.clone()); + } + return min; + } else { + all_paths_from_env(env_path, cmd_path) + .iter() + .find_map(|cmd_path| { + let min = match_single_path(cmd_path, role_path); + if min.better(&previous_min) { + *final_path = Some(cmd_path.clone()); + Some(min) + } else { + None + } + }) + .unwrap_or_default() + } +} + +/// Check if input args is matching with role args and return the score +/// role args can contains regex +/// input args is the command line args +pub(super) fn match_args( + input_args: &[String], + role_args: &str, +) -> Result> { + if role_args == "'^.*$'" { + return Ok(CmdMin::FullRegexArgs); + } + let commandline = shell_words::join(input_args); + if role_args.starts_with("\'^") && role_args.ends_with("$\'") { + evaluate_regex_cmd(role_args.trim_matches('\''), &commandline).inspect_err(|e| { + debug!("{:?},No match for args {:?}", e, input_args); + }) + } else if commandline == role_args { + Ok(CmdMin::Match) + } else { + Ok(CmdMin::empty()) + } +} + +#[cfg(feature = "pcre2")] +fn evaluate_regex_cmd( + role_args: &str, + commandline: &str, +) -> Result> { + use pcre2::bytes::RegexBuilder; + + let regex = RegexBuilder::new().build(&role_args)?; + if regex.is_match(commandline.as_bytes())? { + Ok(CmdMin::RegexArgs) + } else { + Ok(CmdMin::empty()) + } +} + +#[cfg(not(feature = "pcre2"))] +fn evaluate_regex_cmd( + _role_args: &str, + _commandline: &str, +) -> Result> { + Ok(CmdMin::empty()) +} + +/// Check if input command line is matching with role command line and return the score +fn match_command_line( + env_path: &[&str], + cmd_path: &PathBuf, + cmd_args: &[String], + role_command: &[String], + previous_min: &CmdMin, + final_path: &mut Option, +) -> CmdMin { + if role_command.is_empty() { + return CmdMin::empty(); + } + let mut result = match_path( + env_path, + &cmd_path, + &role_command[0], + previous_min, + final_path, + ); + if result.is_empty() || role_command.len() == 1 { + debug!("result : {:?}", result); + return result; + } + match match_args(cmd_args, &shell_words::join(&role_command[1..])) { + Ok(args_result) => { + if args_result.is_empty() { + return CmdMin::empty(); + } + result |= args_result; + } + Err(err) => { + debug!("Error: {}", err); + return CmdMin::empty(); + } + } + result +} + +#[inline(always)] +pub fn evaluate_command_match( + env_path: &[&str], + cmd_path: &PathBuf, + cmd_args: &[String], + role_cmd: &str, + previous_min: &CmdMin, + final_path: &mut Option, +) -> CmdMin { + match shell_words::split(role_cmd).map_err(|e| Into::>::into(e)) { + Ok(role_cmd) => match_command_line( + env_path, + cmd_path, + cmd_args, + &role_cmd, + previous_min, + final_path, + ), + Err(err) => { + warn!("Error: {}", err); + CmdMin::empty() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use test_log::test; + + #[test] + fn test_match_path_full_wildcard() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let role_path = String::from("**"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert_eq!(result, CmdMin::FullWildcardPath); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_path() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let role_path = String::from("/bin/ls"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_path_absolute_no_match() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("/usr/local/bin/ls"); + let role_path = String::from("/bin/ls"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_path_absolute_match() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("/bin/ls"); + let role_path = String::from("/bin/ls"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_path_not_found_in_env() { + let env_path = ["/usr/local/sbin"]; + let cmd_path = PathBuf::from("ls"); + let role_path = String::from("/bin/ls"); + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_path( + &env_path, + &cmd_path, + &role_path, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_args() { + let input_args = vec!["-l".to_string(), "/tmp".to_string()]; + let role_args = "-l /tmp"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::Match); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_match_args_full_regex() { + let input_args = vec!["foo".to_string(), "bar".to_string()]; + let role_args = "'^.*$'"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::FullRegexArgs); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_match_args_full_regex_empty_input() { + let input_args: Vec = vec![]; + let role_args = "'^.*$'"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::FullRegexArgs); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_match_args_regex_args() { + let input_args: Vec = vec!["a".to_string(), "A".to_string()]; + let role_args = "'^[Aa ]*$'"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::RegexArgs); + let role_args = "'^[Aa]*$'"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::empty()); + } + + #[test] + fn test_match_args_no_match() { + let input_args = vec!["-a".to_string()]; + let role_args = "-l"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::empty()); + } + + #[test] + fn test_match_args_input_longer_than_role() { + let input_args = vec!["-l".to_string(), "/tmp".to_string(), "extra".to_string()]; + let role_args = "-l /tmp"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::empty()); + } + + #[test] + fn test_match_args_input_shorter_than_role() { + let input_args = vec!["-l".to_string()]; + let role_args = "-l /tmp"; + let result = match_args(&input_args, &role_args); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), CmdMin::empty()); + } + + #[test] + fn test_match_command_line() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-l".to_string(), "/tmp".to_string()]; + let role_command = vec!["/bin/ls".to_string(), "-l".to_string(), "/tmp".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_match_command_line_args_mismatch() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-a".to_string()]; + let role_command = vec!["/bin/ls".to_string(), "-l".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_command_line_empty_cmd_args_multi_role_args() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_command = vec!["/bin/ls".to_string(), "-l".to_string(), "/tmp".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_command_line_single_arg() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_command = vec!["/bin/ls".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_match_command_line_empty_role_command() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_command: Vec = vec![]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_command_line_role_command_only_args() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-l".to_string()]; + let role_command = vec!["-l".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + // Should not match, as the binary is not specified + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_command_line_role_command_only_wildcard() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_command = vec!["**".to_string()]; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert_eq!(result, CmdMin::FullWildcardPath); + assert_eq!(final_path, None); + } + + #[test] + fn test_match_command_line_previous_min_set() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec!["-l".to_string()]; + let role_command = vec!["/bin/l*".to_string(), "^.*$".to_string()]; + let previous_min = CmdMin::Match; // better than regex + let mut final_path = Some("/usr/bin/ls".into()); + let result = match_command_line( + &env_path, + &cmd_path, + &cmd_args, + &role_command, + &previous_min, + &mut final_path, + ); + assert_eq!(result, CmdMin::empty()); + assert_eq!(final_path, Some("/usr/bin/ls".into())); + } + + #[test] + fn test_evaluate_command_match() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-l".to_string(), "/tmp".to_string()]; + let role_cmd = "/bin/ls -l /tmp"; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = evaluate_command_match( + &env_path, + &cmd_path, + &cmd_args, + role_cmd, + &previous_min, + &mut final_path, + ); + assert!(result.matching()); + assert_eq!(final_path, Some(PathBuf::from("/bin/ls"))); + } + + #[test] + fn test_evaluate_command_match_invalid_role_cmd() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args = vec!["-l".to_string(), "/tmp".to_string()]; + let role_cmd = "\"unterminated string"; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = evaluate_command_match( + &env_path, + &cmd_path, + &cmd_args, + role_cmd, + &previous_min, + &mut final_path, + ); + assert!(!result.matching()); + assert_eq!(final_path, None); + } + + #[test] + fn test_evaluate_command_match_only_wildcard() { + let env_path = ["/usr/bin", "/bin"]; + let cmd_path = PathBuf::from("ls"); + let cmd_args: Vec = vec![]; + let role_cmd = "**"; + let previous_min = CmdMin::empty(); + let mut final_path = None; + let result = evaluate_command_match( + &env_path, + &cmd_path, + &cmd_args, + role_cmd, + &previous_min, + &mut final_path, + ); + assert_eq!(result, CmdMin::FullWildcardPath); + assert_eq!(final_path, None); + } +} diff --git a/src/sr/finder/de.rs b/src/sr/finder/de.rs new file mode 100644 index 00000000..574d7671 --- /dev/null +++ b/src/sr/finder/de.rs @@ -0,0 +1,2826 @@ +use std::{borrow::Cow, collections::HashMap, fmt::Display, ops::Deref, path::PathBuf}; + +use bon::Builder; +use capctl::CapSet; +use derivative::Derivative; +use log::{debug, info}; +use nix::unistd::Group; +use rar_common::{ + database::{ + actor::{DActor, DGroupType, DGroups, DUserType}, + options::Level, + score::{ + ActorMatchMin, CapsMin, CmdMin, Score, SecurityMin, SetgidMin, SetuidMin, TaskScore, + }, + structs::{SCapabilities, SetBehavior}, + }, + util::capabilities_are_exploitable, + Cred, +}; +use serde::{ + de::{DeserializeSeed, IgnoredAny, Visitor}, + Deserialize, +}; +use serde_json_borrow::Value; +use strum::EnumIs; + +use crate::{ + finder::{ + api::{Api, ApiEvent}, + cmd, + options::DPathOptions, + }, + Cli, +}; + +use super::options::Opt; + +#[cfg_attr(test, derive(Builder))] +#[derive(PartialEq, Eq, Debug, Default)] +pub struct DConfigFinder<'a> { + pub options: Option>, + pub roles: Vec>, +} + +#[cfg_attr(test, derive(Builder))] +#[derive(Debug, Derivative)] +#[derivative(PartialEq, Eq)] +pub struct DRoleFinder<'a> { + #[cfg_attr(test, builder(default))] + pub user_min: ActorMatchMin, + #[cfg_attr(test, builder(into))] + pub role: Cow<'a, str>, + #[cfg_attr(test, builder(default))] + pub tasks: Vec>, + pub options: Option>, + #[cfg_attr(test, builder(default))] + pub _extra_values: HashMap, Value<'a>>, +} + +#[derive(Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] +#[serde(untagged)] +pub enum IdTask<'a> { + Name(#[serde(borrow)] Cow<'a, str>), + Number(usize), +} + +impl Display for IdTask<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IdTask::Name(name) => write!(f, "{}", name), + IdTask::Number(num) => write!(f, "{}", num), + } + } +} + +#[derive(Debug, Derivative, Builder)] +#[derivative(PartialEq, Eq)] +pub struct DTaskFinder<'a> { + pub id: IdTask<'a>, + #[builder(default)] + pub score: TaskScore, + pub setuid: Option>, + pub setgroups: Option>, + pub caps: Option, + pub commands: Option>, + pub options: Option>, + pub final_path: Option, + #[builder(default)] + pub _extra_values: HashMap, Value<'a>>, +} + +#[derive(Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] +#[serde(untagged)] +pub enum DCommand<'a> { + Simple(#[serde(borrow)] Cow<'a, str>), + Complex(Value<'a>), +} + +#[cfg(test)] +impl<'a> DCommand<'a> { + pub fn simple(cmd: &'a str) -> Self { + DCommand::Simple(Cow::Borrowed(cmd)) + } + pub fn complex(cmd: Value<'a>) -> Self { + DCommand::Complex(cmd) + } +} + +pub struct ConfigFinderDeserializer<'a> { + pub cli: &'a Cli, + pub cred: &'a Cred, + pub env_path: &'a [&'a str], +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for ConfigFinderDeserializer<'a> { + type Value = DConfigFinder<'a>; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field<'a> { + Options, + Roles, + #[serde(untagged, borrow)] + #[allow(dead_code)] + Unknown(Cow<'a, str>), + } + + struct ConfigFinderVisitor<'a> { + cli: &'a Cli, + cred: &'a Cred, + env_path: &'a [&'a str], + human_readable: bool, + } + + impl<'de: 'a, 'a> Visitor<'de> for ConfigFinderVisitor<'a> { + type Value = DConfigFinder<'a>; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("policy") + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut options = None; + let mut roles = Vec::new(); + let mut spath = DPathOptions::default_path(); + while let Some(key) = map.next_key()? { + match key { + Field::Options => { + debug!("ConfigFinderVisitor: options"); + let mut opt: Opt = map.next_value()?; + opt.level = Level::Global; + if self.human_readable { + if let Some(path) = opt.path.as_ref() { + spath.union(path.clone()); + } + } + options = Some(opt); + } + Field::Roles => { + debug!("ConfigFinderVisitor: roles"); + roles = map.next_value_seed(RoleListFinderDeserializer { + cli: self.cli, + cred: self.cred, + spath: &mut spath, + env_path: self.env_path, + })?; + } + Field::Unknown(_) => { + debug!("ConfigFinderVisitor: unknown"); + let _ = map.next_value::(); + } + } + } + Ok(DConfigFinder { options, roles }) + } + } + const FIELDS: &[&str] = &["options", "roles", "version"]; + let human_readable = deserializer.is_human_readable(); + let res = deserializer.deserialize_struct( + "Config", + FIELDS, + ConfigFinderVisitor { + cli: self.cli, + cred: self.cred, + human_readable, + env_path: self.env_path, + }, + ); + res + } +} + +struct RoleListFinderDeserializer<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + spath: &'b mut DPathOptions<'a>, + env_path: &'a [&'a str], +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for RoleListFinderDeserializer<'a, '_> { + type Value = Vec>; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct RoleListFinderVisitor<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + spath: &'b mut DPathOptions<'a>, + env_path: &'a [&'a str], + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for RoleListFinderVisitor<'a, '_> { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("RoleList sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + debug!("RoleListFinderVisitor: visit_seq"); + let mut roles = Vec::new(); + while let Some(role) = seq.next_element_seed(RoleFinderDeserializer { + cli: self.cli, + cred: self.cred, + spath: self.spath, + env_path: self.env_path, + })? { + roles.push(role); + } + Ok(roles) + } + } + deserializer.deserialize_seq(RoleListFinderVisitor { + cli: self.cli, + cred: self.cred, + spath: self.spath, + env_path: self.env_path, + }) + } +} + +struct RoleFinderDeserializer<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for RoleFinderDeserializer<'a, '_> { + type Value = DRoleFinder<'a>; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field<'a> { + #[serde(alias = "n")] + Name, + #[serde(alias = "a", alias = "users")] + Actors, + #[serde(alias = "t")] + Tasks, + #[serde(alias = "o")] + Options, + #[serde(untagged, borrow)] + Unknown(Cow<'a, str>), + } + + struct RoleFinderVisitor<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, + _human_readable: bool, + } + + impl<'de: 'a, 'a> Visitor<'de> for RoleFinderVisitor<'a, '_> { + type Value = DRoleFinder<'a>; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a role") + } + fn visit_map(mut self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + debug!("RoleFinderVisitor: visit_map"); + let mut role = None; + let mut tasks: Vec> = Vec::new(); + let mut options = None; + let mut extra_values = HashMap::new(); + let mut user_min = ActorMatchMin::default(); + while let Some(key) = map.next_key()? { + match key { + Field::Options => { + debug!("RoleFinderVisitor: options"); + let mut opt: Opt = map.next_value()?; + opt.level = Level::Role; + if let Some(path) = opt.path.as_ref() { + self.spath.union(path.clone().into()); + } + options = Some(opt); + } + Field::Name => { + debug!("RoleFinderVisitor: name"); + role = Some(map.next_value()?); + } + Field::Actors => { + debug!("RoleFinderVisitor: actors"); + user_min = + map.next_value_seed(ActorsFinderDeserializer { cred: self.cred })?; + } + Field::Tasks => { + debug!("RoleFinderVisitor: tasks"); + tasks = map.next_value_seed(TaskListFinderDeserializer { + cli: self.cli, + spath: &mut self.spath, + env_path: self.env_path, + })?; + } + Field::Unknown(key) => { + debug!("RoleFinderVisitor: unknown {}", key); + let unknown: Value = map.next_value()?; + extra_values.insert(key, unknown); + } + } + } + Ok(DRoleFinder { + user_min, + role: role.unwrap_or_default(), + tasks, + options, + _extra_values: extra_values, + }) + } + } + const FIELDS: &[&str] = &["name", "tasks", "options"]; + let _human_readable = deserializer.is_human_readable(); + deserializer.deserialize_struct( + "Role", + FIELDS, + RoleFinderVisitor { + cli: self.cli, + cred: self.cred, + spath: self.spath, + env_path: self.env_path, + _human_readable, + }, + ) + } +} + +struct ActorsFinderDeserializer<'a> { + cred: &'a Cred, +} + +impl<'de> DeserializeSeed<'de> for ActorsFinderDeserializer<'_> { + type Value = ActorMatchMin; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ActorsFinderVisitor<'a> { + cred: &'a Cred, + } + + impl<'de> Visitor<'de> for ActorsFinderVisitor<'_> { + type Value = ActorMatchMin; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a set of users") + } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut user_matches = ActorMatchMin::NoMatch; + while let Some(actor) = seq.next_element::()? { + debug!("ActorsSettingsVisitor: actor {:?}", actor); + let temp = self.user_matches(self.cred, &actor); + if temp != ActorMatchMin::NoMatch && temp < user_matches { + info!("ActorsSettingsVisitor: Better actor found {:?}", temp); + user_matches = temp; + } + } + Ok(user_matches) + } + } + + impl ActorsFinderVisitor<'_> { + fn match_groups(groups: &[Group], role_groups: &[&DGroups<'_>]) -> bool { + for role_group in role_groups { + if match role_group { + DGroups::Single(group) => groups.iter().any(|g| group == g), + DGroups::Multiple(multiple_actors) => multiple_actors + .iter() + .all(|actor| groups.iter().any(|g| actor == g)), + } { + return true; + } + } + false + } + fn user_matches(&self, user: &Cred, actor: &DActor<'_>) -> ActorMatchMin { + match actor { + DActor::User { id, .. } => { + if *id == user.user { + return ActorMatchMin::UserMatch; + } + } + DActor::Group { groups, .. } => { + if Self::match_groups(&user.groups, &[groups]) { + return ActorMatchMin::GroupMatch(groups.len()); + } + } + DActor::Unknown(element) => { + unimplemented!("Unknown actor type: {:?}", element); + } + } + ActorMatchMin::NoMatch + } + } + + deserializer.deserialize_seq(ActorsFinderVisitor { cred: self.cred }) + } +} + +struct TaskListFinderDeserializer<'a, 'b> { + cli: &'a Cli, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for TaskListFinderDeserializer<'a, '_> { + type Value = Vec>; + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct TaskListFinderVisitor<'a, 'b> { + cli: &'a Cli, + spath: &'b mut DPathOptions<'a>, + env_path: &'a [&'a str], + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for TaskListFinderVisitor<'a, '_> { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("TaskList sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut tasks = Vec::new(); + let mut i = 0; + while let Some(element) = seq.next_element_seed(TaskFinderDeserializer { + cli: self.cli, + spath: self.spath, + env_path: self.env_path, + i, + })? { + if let Some(task) = element { + debug!("adding task {:?}", task); + tasks.push(task); + i += 1; + } + } + Ok(tasks) + } + } + deserializer.deserialize_seq(TaskListFinderVisitor { + cli: self.cli, + spath: self.spath, + env_path: self.env_path, + }) + } +} + +struct TaskFinderDeserializer<'a, 'b> { + cli: &'a Cli, + i: usize, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for TaskFinderDeserializer<'a, '_> { + type Value = Option>; + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field<'a> { + #[serde(alias = "n")] + Name, + #[serde(alias = "i", alias = "credentials")] + Cred, + #[serde(alias = "c", alias = "cmds")] + Commands, + #[serde(alias = "o")] + Options, + #[serde(untagged, borrow)] + Unknown(Cow<'a, str>), + } + + struct TaskFinderVisitor<'a, 'b> { + cli: &'a Cli, + i: usize, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, + human_readable: bool, + } + + impl<'de: 'a, 'a> serde::de::Visitor<'de> for TaskFinderVisitor<'a, '_> { + type Value = Option>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("STask structure") + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + // Use local temporaries for each field + let mut id = IdTask::Number(self.i); + let mut score = TaskScore::default(); + let mut setuid = None; + let mut setgroups = None; + let mut caps = None; + let mut commands = None; + let mut options = None; + let mut final_path = None; + let mut extra_values = HashMap::new(); + + while let Some(key) = map.next_key()? { + match key { + Field::Options => { + debug!("TaskFinderVisitor: options"); + let mut opt: Opt = map.next_value()?; + opt.level = Level::Task; + if let Some(path) = opt.path.as_ref() { + self.spath.union(path.clone().into()); + } + options = Some(opt); + } + Field::Name => { + debug!("TaskFinderVisitor: name"); + id = map.next_value()?; + } + Field::Cred => { + debug!("TaskFinderVisitor: cred"); + let (su, sg, ca, sc, ok) = map + .next_value_seed(CredFinderDeserializerReturn { cli: self.cli })?; + setuid = su; + setgroups = sg; + caps = ca; + score.setuser_min = sc.setuser_min; + score.caps_min = sc.caps_min; + if !ok { + while map.next_entry::()?.is_some() {} + return Ok(None); + } + } + Field::Commands => { + debug!("TaskFinderVisitor: commands"); + // if is_human_readable -> next_value + // else -> next_value_seed -> no memory allocation, just the result, thus highly optimizing + if self.human_readable { + commands = Some(map.next_value()?); + } else { + map.next_value_seed(DCommandListDeserializer { + env_path: &self.spath.calc_path(self.env_path), + cmd_path: &self.cli.cmd_path, + cmd_args: &self.cli.cmd_args, + final_path: &mut final_path, + cmd_min: &mut score.cmd_min, + blocker: false, + })?; + } + } + Field::Unknown(key) => { + debug!("TaskFinderVisitor: unknown"); + let unknown: Value = map.next_value()?; + extra_values.insert(key, unknown); + } + } + } + debug!("TaskFinderVisitor: final_path {:?}", final_path); + Ok(Some(DTaskFinder { + id, + score, + setuid, + setgroups, + caps, + commands, + options, + final_path, + _extra_values: extra_values, + })) + } + } + + const FIELDS: &[&str] = &["name", "cred", "commands", "options"]; + let human_readable = deserializer.is_human_readable(); + deserializer.deserialize_struct( + "STask", + FIELDS, + TaskFinderVisitor { + i: self.i, + cli: self.cli, + env_path: self.env_path, + spath: self.spath, + human_readable, + }, + ) + } +} + +struct CredFinderDeserializerReturn<'a> { + cli: &'a Cli, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for CredFinderDeserializerReturn<'a> { + type Value = ( + Option>, + Option>, + Option, + TaskScore, + bool, + ); + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field<'a> { + #[serde(alias = "u")] + Setuid, + #[serde(alias = "g", alias = "setgroups")] + Setgid, + #[serde(alias = "c", alias = "capabilities")] + Caps, + #[serde(untagged, borrow)] + Other(Cow<'a, str>), + } + + struct CredFinderVisitor<'a> { + cli: &'a Cli, + } + + fn get_caps_min(caps: &CapSet) -> CapsMin { + if caps.is_empty() { + CapsMin::NoCaps + } else if *caps == !CapSet::empty() { + CapsMin::CapsAll + } else if capabilities_are_exploitable(caps) { + CapsMin::CapsAdmin(caps.size()) + } else { + CapsMin::CapsNoAdmin(caps.size()) + } + } + + impl<'de: 'a, 'a> serde::de::Visitor<'de> for CredFinderVisitor<'a> { + type Value = ( + Option>, + Option>, + Option, + TaskScore, + bool, + ); + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Cred structure") + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut setuid = None; + let mut setgroups = None; + let mut caps = None; + let mut score = TaskScore::default(); + let mut ok = true; + while let Some(key) = map.next_key()? { + match key { + Field::Setuid => { + debug!("CredFinderVisitor: setuid"); + let (user, setuser_min, user_ok) = + map.next_value_seed(SetUserDeserializerReturn { cli: self.cli })?; + setuid = user; + score.setuser_min.uid = setuser_min; + if !user_ok { + ok = false; + } + } + Field::Setgid => { + debug!("CredFinderVisitor: setgid"); + let (groups, setuser_min, groups_ok) = + map.next_value_seed(SetGroupsDeserializerReturn { cli: self.cli })?; + setgroups = groups; + score.setuser_min.gid = setuser_min; + if !groups_ok { + ok = false; + } + } + Field::Caps => { + debug!("CredFinderVisitor: capabilities"); + let scaps: SCapabilities = map.next_value()?; + let capset = scaps.to_capset(); + score.caps_min = get_caps_min(&capset); + caps = Some(capset); + } + Field::Other(n) => { + return Err(serde::de::Error::custom(format!( + "Unknown Cred field {}", + n + ))); + } + } + } + debug!("CredFinderVisitor: end"); + Ok((setuid, setgroups, caps, score, ok)) + } + } + const FIELDS: &[&str] = &["setuid", "setgroups", "capabilities", "0", "1", "2"]; + let (setuid, setgroups, caps, score, ok) = + deserializer.deserialize_struct("Cred", FIELDS, CredFinderVisitor { cli: self.cli })?; + Ok((setuid, setgroups, caps, score, ok)) + } +} + +// New deserializer for SetGroups that returns values instead of using &mut +struct SetGroupsDeserializerReturn<'a> { + cli: &'a Cli, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for SetGroupsDeserializerReturn<'a> { + type Value = (Option>, Option, bool); + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "f")] + Fallback, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub")] + Del, + } + struct SGroupsChooserVisitor<'a> { + cli: &'a Cli, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for SGroupsChooserVisitor<'a> { + type Value = (Option>, Option, bool); + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("SGroups structure") + } + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_borrowed_str"); + let group: DGroupType<'_> = if let Ok(gid) = v.parse::() { + gid.into() + } else { + v.into() + }; + let score = Some(SetgidMin::from(&group)); + let ok = true; + if let Some(y) = &self + .cli + .opt_filter + .as_ref() + .map(|x| x.group.as_ref()) + .flatten() + { + if y.len() == 1 + && y[0] + != group + .fetch_id() + .ok_or(serde::de::Error::custom("Group does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(DGroups::Single(group)), score, ok)) + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_str"); + self.visit_string(v.to_string()) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_string"); + let group: DGroupType<'_> = if let Ok(gid) = v.parse::() { + gid.into() + } else { + Cow::::from(v).into() + }; + let score = Some(SetgidMin::from(&group)); + let ok = true; + if let Some(y) = &self + .cli + .opt_filter + .as_ref() + .map(|x| x.group.as_ref()) + .flatten() + { + if y.len() == 1 + && y[0] + != group + .fetch_id() + .ok_or(serde::de::Error::custom("Group does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(DGroups::Single(group)), score, ok)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_u64"); + if v > u32::MAX as u64 { + return Err(serde::de::Error::custom("Group id too large")); + } + let group: DGroupType<'_> = (v as u32).into(); + let score = Some(SetgidMin::from(&group)); + let ok = true; + if let Some(y) = &self + .cli + .opt_filter + .as_ref() + .map(|x| x.group.as_ref()) + .flatten() + { + if y.len() == 1 + && y[0] + != group + .fetch_id() + .ok_or(serde::de::Error::custom("Group does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(DGroups::Single(group)), score, ok)) + } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + debug!("SGroupsChooserVisitor: visit_seq"); + let mut groups = None; + let mut score = None; + let mut ok = false; + let filter = self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()); + while let Some(group) = seq.next_element::()? { + if let Some(u) = filter { + let parsed_ids: Vec = + (&group).try_into().map_err(serde::de::Error::custom)?; + if *u == parsed_ids { + ok = true; + groups = Some(group.to_owned()); + score.replace((&group).into()); + while seq.next_element::()?.is_some() {} + break; + } + } else { + groups = Some(group.to_owned()); + ok = true; + score.replace((&group).into()); + } + } + Ok((groups, score, ok)) + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut groups = None; + let mut score = None; + let mut ok = false; + let filter = self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()); + 'fields: while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("SGroupsChooserVisitor: default"); + let default = map.next_value::()?; + if default.is_all() { + ok = true; + } + } + Field::Fallback => { + debug!("SGroupsChooserVisitor: fallback"); + let value = map.next_value::()?; + if let Some(u) = filter { + let parsed_ids: Vec = + (&value).try_into().map_err(serde::de::Error::custom)?; + if *u == parsed_ids { + ok = true; + groups = Some(value.to_owned()); + score.replace((&value).into()); + } + } else { + groups = Some(value.to_owned()); + ok = true; + score.replace((&value).into()); + } + } + Field::Add => { + debug!("SGroupsChooserVisitor: add"); + if filter.is_some() { + let add = map.next_value::>()?; + for group in add.iter() { + let v: Vec = + group.try_into().map_err(serde::de::Error::custom)?; + if v == *filter.unwrap() { + ok = true; + groups = Some(group.to_owned()); + score.replace(group.into()); + while map.next_entry::()?.is_some() + { + } + break; + } + } + } else { + map.next_value::()?; + } + } + Field::Del => { + debug!("SGroupsChooserVisitor: del"); + if let Some(u) = filter { + for group in map.next_value::>()?.iter() { + if let Some(v) = TryInto::>::try_into(group).ok() { + if v == *u { + while map + .next_entry::()? + .is_some() + { + } + ok = false; + groups = None; + score = None; + break 'fields; + } + } else { + return Err(serde::de::Error::custom("Invalid group")); + } + } + } else { + map.next_value::()?; + } + } + } + } + Ok((groups, score, ok)) + } + } + deserializer.deserialize_any(SGroupsChooserVisitor { cli: self.cli }) + } +} + +// New deserializer for SetUser that returns values instead of using &mut +struct SetUserDeserializerReturn<'a> { + cli: &'a Cli, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for SetUserDeserializerReturn<'a> { + type Value = (Option>, Option, bool); + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "f")] + Fallback, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub")] + Del, + } + struct SetUserVisitor<'a> { + cli: &'a Cli, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for SetUserVisitor<'a> { + type Value = (Option>, Option, bool); + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("SUser structure") + } + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_borrowed_str"); + let user = if let Ok(uid) = v.parse::() { + DUserType::from(uid) + } else { + DUserType::from(v) + }; + let score = Some(SetuidMin::from(&user)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().map(|x| x.user).flatten() { + if *y + != user + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(user), score, ok)) + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_str"); + self.visit_string(v.to_string()) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_string"); + let user = if let Ok(uid) = v.parse::() { + DUserType::from(uid) + } else { + DUserType::from(v) + }; + let score = Some(SetuidMin::from(&user)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().map(|x| x.user).flatten() { + if *y + != user + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(user), score, ok)) + } + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_i64"); + if v > u32::MAX as u64 { + return Err(serde::de::Error::custom("User id too large")); + } + let user = DUserType::from(v as u32); + let score = Some(SetuidMin::from(&user)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().map(|x| x.user).flatten() { + if *y + != user + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))? + { + return Ok((None, None, false)); + } + } + Ok((Some(user), score, ok)) + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut user = None; + let mut score = None; + let mut ok = false; + let filter = self.cli.opt_filter.as_ref().and_then(|x| x.user.as_ref()); + 'fields: while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("SUserChooserVisitor: default"); + let default = map.next_value::()?; + if default.is_all() { + ok = true; + } + } + Field::Fallback => { + debug!("SUserChooserVisitor: fallback"); + let value = map.next_value::()?; + if let Some(u) = filter { + let userid = value + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))?; + if u == &userid { + score.replace((&value).into()); + user = Some(value.into()); + ok = true; + } + } else { + ok = true; + score.replace((&value).into()); + user = Some(value); + } + } + Field::Add => { + debug!("SUserChooserVisitor: add"); + if filter.is_some() { + let users = map.next_value::>()?; + for user_item in users.iter() { + let user_id = user_item + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))?; + if user_id == *filter.unwrap() { + ok = true; + user = Some(user_item.to_owned()); + score.replace(user_item.into()); + break; + } + } + } else { + map.next_value::()?; + } + } + Field::Del => { + debug!("SUserChooserVisitor: del"); + if let Some(u) = filter { + let users = map.next_value::>()?; + for user_item in users.iter() { + let user_id = user_item + .fetch_id() + .ok_or(serde::de::Error::custom("User does not exist"))?; + if user_id == *u { + while map.next_entry::()?.is_some() + { + } + score = None; + user = None; + ok = false; + break 'fields; + } + } + } else { + map.next_value::()?; + } + } + } + } + Ok((user, score, ok)) + } + } + deserializer.deserialize_any(SetUserVisitor { cli: self.cli }) + } +} + +/// This struct keeps the list of commands because options may be written after +#[cfg_attr(test, derive(Builder))] +#[derive(PartialEq, Eq, Debug)] +pub struct DCommandList<'a> { + #[cfg_attr(test, builder(start_fn, into))] + pub default_behavior: Option, + #[cfg_attr(test, builder(default, into))] + pub add: Cow<'a, [DCommand<'a>]>, + #[cfg_attr(test, builder(default, into))] + pub del: Cow<'a, [DCommand<'a>]>, +} + +impl<'de: 'a, 'a> Deserialize<'de> for DCommandList<'a> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + Default, + Add, + Del, + } + #[derive(Default)] + struct DCommandListVisitor<'a> { + _phantom: std::marker::PhantomData<&'a ()>, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandListVisitor<'a> { + type Value = DCommandList<'a>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("CommandList structure") + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut default_behavior = None; + let mut add: Cow<'_, [DCommand<'_>]> = Cow::Borrowed(&[]); + let mut del: Cow<'_, [DCommand<'_>]> = Cow::Borrowed(&[]); + while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("DCommandListVisitor: default"); + default_behavior = Some(map.next_value()?); + } + Field::Add => { + debug!("DCommandListVisitor: add"); + add = map.next_value()?; + } + Field::Del => { + debug!("DCommandListVisitor: del"); + del = map.next_value()?; + } + } + } + Ok(DCommandList { + default_behavior, + add, + del, + }) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut add = Vec::new(); + while let Some(command) = seq.next_element()? { + add.push(command); + } + return Ok(DCommandList { + default_behavior: None, + add: Cow::Owned(add), + del: Cow::Borrowed(&[]), + }); + } + + fn visit_bool(self, v: bool) -> Result + where + E: serde::de::Error, + { + return Ok(DCommandList { + default_behavior: Some(if v { + SetBehavior::All + } else { + SetBehavior::None + }), + add: Cow::Borrowed(&[]), + del: Cow::Borrowed(&[]), + }); + } + } + deserializer.deserialize_any(DCommandListVisitor::default()) + } +} + +/// This struct evaluates commands directly from deserialization +pub struct DCommandListDeserializer<'a> { + env_path: &'a [&'a str], + cmd_path: &'a PathBuf, + cmd_args: &'a [String], + pub final_path: &'a mut Option, + pub cmd_min: &'a mut CmdMin, + pub blocker: bool, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for DCommandListDeserializer<'a> { + type Value = bool; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(self) + } +} + +impl<'a> DCommandListDeserializer<'a> { + fn generate_dcommand_deserializer(&mut self) -> DCommandDeserializer<'_> { + DCommandDeserializer { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + } + } +} + +impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandListDeserializer<'a> { + type Value = bool; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("CommandList structure") + } + + fn visit_seq(mut self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut result = false; + while let Some(bool) = seq.next_element_seed(self.generate_dcommand_deserializer())? { + if bool && self.blocker { + return Ok(true); + } + result |= bool; + } + Ok(result) + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub")] + Del, + } + let mut result = false; + let mut default = SetBehavior::None; + while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("DCommandListVisitor: default"); + default = map.next_value()?; + } + Field::Del => { + let deserializer = DCommandListDeserializer { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + blocker: true, + }; + let res = map.next_value_seed(deserializer)?; + if res { + while map.next_entry::()?.is_some() {} + return Ok(false); + } + } + Field::Add => { + if default.is_all() { + let _ = map.next_value::(); + } else { + let deserializer = DCommandListDeserializer { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + blocker: false, + }; + result |= map.next_value_seed(deserializer)?; + } + } + } + } + Ok(result) + } +} + +pub(super) struct DCommandDeserializer<'a> { + pub(super) env_path: &'a [&'a str], + pub(super) cmd_path: &'a PathBuf, + pub(super) cmd_args: &'a [String], + pub(super) final_path: &'a mut Option, + pub(super) cmd_min: &'a mut CmdMin, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for DCommandDeserializer<'a> { + type Value = bool; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct DCommandVisitor<'a> { + env_path: &'a [&'a str], + cmd_path: &'a PathBuf, + cmd_args: &'a [String], + final_path: &'a mut Option, + cmd_min: &'a mut CmdMin, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandVisitor<'a> { + type Value = bool; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Command structure") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let mut final_path = None; + let mut result = false; + debug!("DCommandVisitor: command {}", v); + let cmd_min = cmd::evaluate_command_match( + self.env_path, + self.cmd_path, + self.cmd_args, + v, + self.cmd_min, + &mut final_path, + ); + if cmd_min.better(&self.cmd_min) { + debug!("DCommandVisitor: better command found"); + result = true; + *self.final_path = final_path; + *self.cmd_min = cmd_min; + } + Ok(result) + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut map_value = Vec::new(); + while let Some((key, value)) = map.next_entry::<&str, Value>()? { + map_value.push((key, value)); + } + Api::notify(ApiEvent::ProcessComplexCommand( + &Value::Object(map_value.into()), + self.env_path, + self.cmd_path, + self.cmd_args, + self.cmd_min, + self.final_path, + )) + .map(|_| true) + .map_err(|_| serde::de::Error::custom("Failed to notify process complex command")) + } + } + deserializer.deserialize_any(DCommandVisitor { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + }) + } +} + +impl<'a> DConfigFinder<'a> { + pub fn roles<'s>(&'s self) -> impl Iterator> { + self.roles.iter().map(|role| DLinkedRole::new(self, role)) + } + + pub fn role<'s>(&'s self, role_name: &str) -> Option> { + self.roles + .iter() + .find(|r| r.role == role_name) + .map(|role| DLinkedRole::new(self, role)) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct DLinkedRole<'c, 'a> { + parent: &'c DConfigFinder<'a>, + role: &'c DRoleFinder<'a>, +} + +impl<'c, 'a> DLinkedRole<'c, 'a> { + fn new(parent: &'c DConfigFinder<'a>, role: &'c DRoleFinder<'a>) -> Self { + Self { parent, role } + } + + pub fn tasks<'t>(&'t self) -> impl Iterator> { + self.role + .tasks + .iter() + .map(|task| DLinkedTask::new(self, task)) + } + + pub fn role(&self) -> &DRoleFinder<'a> { + self.role + } + + pub fn config(&self) -> &DConfigFinder<'a> { + self.parent + } +} + +#[derive(Clone, Copy, Debug)] +pub struct DLinkedTask<'t, 'c, 'a> { + parent: &'t DLinkedRole<'c, 'a>, + pub task: &'t DTaskFinder<'a>, +} + +impl<'t, 'c, 'a> DLinkedTask<'t, 'c, 'a> { + fn new(parent: &'t DLinkedRole<'c, 'a>, task: &'t DTaskFinder<'a>) -> Self { + Self { parent, task } + } + + pub fn commands<'l>(&'l self) -> Option> { + self.task + .commands + .as_ref() + .map(|list| DLinkedCommandList::new(self, list)) + } + + pub fn role(&self) -> &DLinkedRole<'c, 'a> { + self.parent + } + + pub fn task(&self) -> &DTaskFinder<'a> { + self.task + } + + pub fn score(&self, cmd_min: CmdMin, security_min: SecurityMin) -> Score { + Score::builder() + .user_min(self.role().role.user_min) + .caps_min(self.score.caps_min) + .cmd_min(cmd_min) + .security_min(security_min) + .setuser_min(self.score.setuser_min) + .build() + } +} + +impl<'t, 'c, 'a> Deref for DLinkedTask<'t, 'c, 'a> { + type Target = DTaskFinder<'a>; + fn deref(&self) -> &Self::Target { + self.task + } +} + +pub struct DLinkedCommandList<'l, 't, 'c, 'a> { + #[allow(dead_code)] // TODO: remove this + parent: &'l DLinkedTask<'t, 'c, 'a>, + command_list: &'l DCommandList<'a>, +} + +impl<'l, 't, 'c, 'a> DLinkedCommandList<'l, 't, 'c, 'a> { + fn new(parent: &'l DLinkedTask<'t, 'c, 'a>, list: &'l DCommandList<'a>) -> Self { + Self { + parent, + command_list: list, + } + } + + pub fn add<'d>(&'d self) -> impl Iterator> { + self.command_list + .add + .iter() + .map(|cmd| DLinkedCommand::new(self, cmd)) + } + + pub fn del<'d>(&'d self) -> impl Iterator> { + self.command_list + .del + .iter() + .map(|cmd| DLinkedCommand::new(self, cmd)) + } +} + +impl<'l, 't, 'c, 'a> Deref for DLinkedCommandList<'l, 't, 'c, 'a> { + type Target = DCommandList<'a>; + fn deref(&self) -> &Self::Target { + self.command_list + } +} + +pub struct DLinkedCommand<'d, 'l, 't, 'c, 'a> { + #[allow(dead_code)] // TODO: remove this + parent: &'d DLinkedCommandList<'l, 't, 'c, 'a>, + pub command: &'d DCommand<'a>, +} + +impl<'d, 'l, 't, 'c, 'a> DLinkedCommand<'d, 'l, 't, 'c, 'a> { + fn new(parent: &'d DLinkedCommandList<'l, 't, 'c, 'a>, command: &'d DCommand<'a>) -> Self { + Self { parent, command } + } + + #[allow(dead_code)] // TODO: remove this + pub fn task(&self) -> &DLinkedTask<'t, 'c, 'a> { + self.parent.parent + } +} + +impl<'d, 'l, 't, 'c, 'a> Deref for DLinkedCommand<'d, 'l, 't, 'c, 'a> { + type Target = DCommand<'a>; + fn deref(&self) -> &Self::Target { + self.command + } +} + +#[cfg(test)] +mod tests { + + use std::fs; + + use super::*; + use capctl::Cap; + use cbor4ii::core::utils::SliceReader; + use nix::unistd::{getgid, getuid}; + use rar_common::database::{ + actor::{DGroupType, SGroupType, SGroups}, + score::{SetUserMin, SetgidMin, SetuidMin}, + FilterMatcher, + }; + use test_log::test; + + fn get_non_root_uid(nth: usize) -> Option { + // list all users + let passwd = fs::read_to_string("/etc/passwd").unwrap(); + let passwd: Vec<&str> = passwd.split('\n').collect(); + return passwd + .iter() + .map(|line| { + let line: Vec<&str> = line.split(':').collect(); + line[2].parse::().unwrap() + }) + .filter(|uid| *uid != 0) + .nth(nth); + } + + fn get_non_root_gid(nth: usize) -> Option { + // list all users + let passwd = fs::read_to_string("/etc/group").unwrap(); + let passwd: Vec<&str> = passwd.split('\n').collect(); + return passwd + .iter() + .map(|line| { + let line: Vec<&str> = line.split(':').collect(); + line[2].parse::().unwrap() + }) + .filter(|uid| *uid != 0) + .nth(nth); + } + + fn convert_json_to_cbor(json: &str) -> Vec { + let value: Value = serde_json::from_str(json).unwrap(); + let cbor = cbor4ii::serde::to_vec(Vec::new(), &value).unwrap(); + cbor + } + + #[test] + fn test_idtask_display() { + let name = IdTask::Name(Cow::Borrowed("test")); + let number = IdTask::Number(42); + assert_eq!(format!("{}", name), "test"); + assert_eq!(format!("{}", number), "42"); + } + + #[test] + fn test_dcommandlist_deserialize_seq() { + let json = r#"["ls", "cat"]"#; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.add.len(), 2); + assert!(matches!(list.add[0], DCommand::Simple(_))); + } + + #[test] + fn test_dcommandlist_deserialize_map() { + let json = r#"{"default": "all", "add": ["ls"], "del": ["rm"]}"#; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior.unwrap(), SetBehavior::All); + assert_eq!(list.add.len(), 1); + assert_eq!(list.del.len(), 1); + } + + #[test] + fn test_dcommandlist_deserialize_bool() { + let json = "true"; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior, Some(SetBehavior::All)); + assert_eq!(list.add.len(), 0); + assert_eq!(list.del.len(), 0); + let json = "false"; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior, Some(SetBehavior::None)); + assert_eq!(list.add.len(), 0); + assert_eq!(list.del.len(), 0); + } + + #[test] + fn test_dcommandlist_deserialize_empty() { + let json = "{}"; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior, None); + assert_eq!(list.add.len(), 0); + assert_eq!(list.del.len(), 0); + } + + #[test] + fn test_dcommandlist_deserialize_invalid() { + let json = r#"{"default": "invalid", "add": ["ls"], "del": ["rm"]}"#; + let result: Result = serde_json::from_str(json); + assert!(result.is_err()); + } + + #[test] + fn test_dcommandlist_seed() { + let json = r#"{"default": "none", "add": ["/usr/bin/ls"], "del": ["/usr/bin/rm"]}"#; + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandListDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from("/usr/bin/ls"), + cmd_args: &vec![], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + blocker: false, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(final_path, Some(PathBuf::from("/usr/bin/ls"))); + assert!(result); + } + + #[test] + fn test_dcommand_seed() { + let json = r#""/usr/bin/ls""#; + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from("/usr/bin/ls"), + cmd_args: &vec![], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(final_path, Some(PathBuf::from("/usr/bin/ls"))); + assert!(result); + } + + #[test] + fn test_setuserdeserializerreturn() { + let json = + r#"{"default": "none", "fallback": "user1", "add": ["user2"], "del": ["user3"]}"#; + let cli = Cli::builder().build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from("user1"); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + } + + #[test] + fn test_setuserdeserializerreturn_filter() { + let uid1 = get_non_root_uid(0).unwrap(); + let uid2 = get_non_root_uid(1).unwrap(); + let json = format!( + r#"{{"default": "none", "fallback": "root", "add": [{}], "del": [{}]}}"#, + uid1, uid2 + ); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from(uid1); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from("root"); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid2).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(user, None); + let json = "\"root\""; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from("root"); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(user, None); + let json = "0"; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(user, None); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from(0); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + } + + #[test] + fn test_no_fallback() { + let json = r#"{"default": "all"}"#; + let cli = Cli::builder().build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(score, None); + assert_eq!(user, None); + } + + #[test] + fn test_setgroupsdeserializerreturn() { + let json = r#"{"default": "none", "fallback": [1, 2], "add": [[3, 4]], "del": [[5, 6]]}"#; + let cli = Cli::builder().build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::from(vec![1.into(), 2.into()]); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + } + + #[test] + fn test_setgroupsdeserializerreturn_filter() { + let gid1 = get_non_root_gid(0).unwrap(); + let gid2 = get_non_root_gid(1).unwrap(); + let json = format!( + r#"{{"default": "none", "fallback": ["root"], "add": [[{}]], "del": [[{}]]}}"#, + gid1, gid2 + ); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single("root".into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single(gid1.into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid2).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + let json = "\"root\""; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single("root".into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + let json = "0"; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single(0.into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let json = "[[\"root\", 1]]"; + let cli = Cli::builder() + .opt_filter( + FilterMatcher::builder() + .group(vec!["root".into(), Into::::into(1)]) + .unwrap() + .build(), + ) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::from(vec!["root".into(), 1.into()]); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::from(vec!["root".into(), 1.into()]); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + } + + #[test] + fn test_no_fallback_groups() { + let json = r#"{"default": "all"}"#; + let cli = Cli::builder().build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(score, None); + assert_eq!(groups, None); + } + + #[test] + fn test_cred_deserializer() { + let json = r#"{"setuid":"root", "setgid":"root", "caps": ["CAP_SYS_ADMIN"]}"#; + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (user, groups, caps, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(user, Some("root".into())); + assert_eq!(groups, Some(DGroups::from(vec!["root".into()]))); + assert_eq!(caps, Some(CapSet::from_iter(vec![Cap::SYS_ADMIN]))); + assert_eq!(score.setuser_min.uid, Some(SetuidMin::from(&"root".into()))); + assert_eq!( + score.setuser_min.gid, + Some(SetgidMin::from(&Into::>::into("root"))) + ); + assert_eq!(score.caps_min, CapsMin::CapsAdmin(1)); + + let uid = get_non_root_uid(0).unwrap(); + let gid = get_non_root_gid(0).unwrap(); + let json = format!(r#"{{"setuid":{}, "setgid":[[{}]]}}"#, uid, gid); + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (user, groups, caps, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(user, Some(uid.into())); + assert_eq!(groups, Some(DGroups::from(vec![gid.into()]))); + assert_eq!(caps, None); + assert_eq!(score.setuser_min.uid, Some(SetuidMin::from(&uid.into()))); + assert_eq!( + score.setuser_min.gid, + Some(SetgidMin::from(&Into::>::into(uid))) + ); + assert_eq!(score.caps_min, CapsMin::Undefined); + + let uid = get_non_root_uid(0).unwrap(); + let gid = get_non_root_gid(0).unwrap(); + let json = format!(r#"{{"setuid":"{}", "setgid":["{}"]}}"#, uid, gid); + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let (user, groups, caps, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(user, Some(uid.into())); + assert_eq!(groups, Some(DGroups::from(vec![gid.into()]))); + assert_eq!(caps, None); + assert_eq!(score.setuser_min.uid, Some(SetuidMin::from(&uid.into()))); + assert_eq!( + score.setuser_min.gid, + Some(SetgidMin::from(&Into::>::into(uid))) + ); + assert_eq!(score.caps_min, CapsMin::Undefined); + } + + #[test] + fn test_cred_deserializer_invalid() { + let json = r#"{"setuid":-1, "setgid":"invalid", "caps": ["CAP_SYS_ADMIN"]}"#; + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let json = r#"{"setuid":"invalid", "setgid":-1, "caps": ["CAP_SYS_ADMIN"]}"#; + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + } + + #[test] + fn test_task_deserializer() { + let json = r#"{"name": "test", "cred": {"setuid":"0", "setgid":["0", 0], "caps": []}, "commands": ["ls"]}}"#; + let cli = Cli::builder().build(); + let deserializer = TaskFinderDeserializer { + cli: &cli, + i: 0, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let task = result.unwrap().unwrap(); + assert_eq!(task.id, IdTask::Name("test".into())); + assert_eq!(task.score.setuser_min.uid, Some(SetuidMin::from(&0.into()))); + assert_eq!(task.score.setuser_min.gid, Some(SetgidMin::from(&vec![0]))); + assert_eq!(task.score.caps_min, CapsMin::NoCaps); + let commands = task.commands.unwrap(); + assert_eq!(commands.add.len(), 1); + assert_eq!(commands.add[0], DCommand::Simple("ls".into())); + } + + #[test] + fn test_task_list_deserializer() { + let json = r#"[{"name": "test", "cred": {"setuid":"0", "setgid":["0", 0], "caps": []}, "commands": ["ls"]}]"#; + let cli = Cli::builder().build(); + let deserializer = TaskListFinderDeserializer { + cli: &cli, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let task = &result.unwrap()[0]; + assert_eq!(task.id, IdTask::Name("test".into())); + assert_eq!(task.score.setuser_min.uid, Some(SetuidMin::from(&0.into()))); + assert_eq!(task.score.setuser_min.gid, Some(SetgidMin::from(&vec![0]))); + assert_eq!(task.score.caps_min, CapsMin::NoCaps); + let commands = task.commands.as_ref().unwrap(); + assert_eq!(commands.add.len(), 1); + assert_eq!(commands.add[0], DCommand::Simple("ls".into())); + } + + #[test] + fn test_actors_finder_deserializer() { + let json = format!(r#"[{{"type": "user", "id": {}}}]"#, getuid().as_raw()); + let deserializer = ActorsFinderDeserializer { + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let user_min = result.unwrap(); + assert_eq!(user_min, ActorMatchMin::UserMatch); + } + + #[test] + fn test_role_finder_deserializer() { + let json = format!( + r#"{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let role = result.unwrap(); + assert_eq!(role.role, "r_test"); + assert_eq!(role.tasks.len(), 1); + assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); + } + + #[test] + fn test_role_list_finder_deserializer() { + let json = format!( + r#"[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleListFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let role = &result.unwrap()[0]; + assert_eq!(role.role, "r_test"); + assert_eq!(role.tasks.len(), 1); + assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); + let json = format!( + r#"[{{"name":"r_test","actors":[{{"type": "group", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"#, + getgid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleListFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let role = &result.unwrap()[0]; + assert_eq!(role.role, "r_test"); + assert_eq!(role.tasks.len(), 1); + assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); + let json = format!( + r#"[{{"name":"r_test","actors":[{{"type": "user", "id": "874510"}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"# + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleListFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let result = result.unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].user_min, ActorMatchMin::NoMatch); + } + + #[test] + fn test_config_finder_deserializer() { + let json = format!( + r#"{{"roles":[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]}}"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + assert_eq!(config.roles.len(), 1); + assert_eq!(config.roles[0].role, "r_test"); + } + + #[test] + fn test_config_finder_implementation() { + let json = format!( + r#"{{"roles":[{{"name":"r_test","actors":[{{"type":"user","id":{}}}],"tasks":[{{"name":"test","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/ls"]}},{{"name":"test2","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/ls","/usr/bin/cat"]}}]}},{{"name":"r_test2","actors":[{{"type":"group","names":[{}, {}]}}],"tasks":[{{"name":"test3","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/cat","/usr/bin/ls"]}}]}}]}}"#, + getuid().as_raw(), + getgid().as_raw(), + getgid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + let mut roles = config.roles(); + let role_a = roles.next().unwrap(); + assert_eq!(role_a.role().role, "r_test"); + let mut tasks = role_a.tasks(); + let task_a = tasks.next().unwrap(); + assert_eq!(task_a.task().id, IdTask::Name("test".into())); + let commands = task_a.commands().unwrap(); + assert_eq!(commands.add().count(), 1); + assert_eq!( + *commands.add().next().unwrap().command, + DCommand::Simple("/usr/bin/ls".into()) + ); + let task_b = tasks.next().unwrap(); + assert_eq!(task_b.task().id, IdTask::Name("test2".into())); + let commands = task_b.commands().unwrap(); + assert_eq!(commands.add().count(), 2); + assert_eq!( + *commands.add().next().unwrap().command, + DCommand::Simple("/usr/bin/ls".into()) + ); + assert_eq!( + *commands.add().nth(1).unwrap().command, + DCommand::Simple("/usr/bin/cat".into()) + ); + assert!(tasks.next().is_none()); + let role_b = roles.next().unwrap(); + assert_eq!(role_b.role().role, "r_test2"); + let mut tasks = role_b.tasks(); + let task_a = tasks.next().unwrap(); + assert_eq!(task_a.task().id, IdTask::Name("test3".into())); + let commands = task_a.commands().unwrap(); + assert_eq!(commands.add().count(), 2); + assert_eq!( + *commands.add().next().unwrap().command, + DCommand::Simple("/usr/bin/cat".into()) + ); + assert_eq!( + *commands.add().nth(1).unwrap().command, + DCommand::Simple("/usr/bin/ls".into()) + ); + assert_eq!(commands.del().count(), 0); + assert!(tasks.next().is_none()); + assert!(roles.next().is_none()); + assert!(config.options.is_none()); + assert!(config.roles[0].options.is_none()); + assert!(config.roles[0].tasks[0].options.is_none()); + assert!(config.roles[0].tasks[1].options.is_none()); + assert!(config.roles[1].options.is_none()); + assert!(config.roles[1].tasks[0].options.is_none()); + assert!(config.role("r_test").is_some()); + assert!(config.role("r_test2").is_some()); + assert!(config.role("r_test3").is_none()); + assert_eq!(*config.role("r_test").unwrap().config(), config); + assert_eq!(*config.role("r_test2").unwrap().config(), config); + assert_eq!( + *config + .role("r_test") + .unwrap() + .tasks() + .next() + .unwrap() + .role(), + config.role("r_test").unwrap() + ); + assert_eq!( + *config + .role("r_test2") + .unwrap() + .tasks() + .next() + .unwrap() + .role(), + config.role("r_test2").unwrap() + ); + assert_eq!( + config + .role("r_test") + .unwrap() + .tasks() + .next() + .unwrap() + .score(CmdMin::Match, SecurityMin::empty()), + Score::builder() + .user_min(ActorMatchMin::UserMatch) + .setuser_min(SetUserMin { + uid: Some(SetuidMin::from(0)), + gid: Some(SetgidMin::from(SGroups::from(vec![0]))) + }) + .caps_min(CapsMin::NoCaps) + .security_min(SecurityMin::empty()) + .cmd_min(CmdMin::Match) + .build() + ); + } + + #[test] + fn test_config_with_options() { + let json = format!( + r#"{{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:05:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep1" + ], + "check": [ + "check1" + ], + "delete": [ + "del1" + ], + "set": {{ + "set1": "value1", + "set2": "value2" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "roles": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:06:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep2" + ], + "check": [ + "check2" + ], + "delete": [ + "del2" + ], + "set": {{ + "set1": "value2", + "set3": "value3" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "name": "role1", + "actors": [ + {{ + "type": "user", + "id": {} + }} + ], + "tasks": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:07:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep3" + ], + "check": [ + "check3" + ], + "delete": [ + "del3" + ], + "set": {{ + "set1": "value3", + "set4": "value4" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "name": "task1", + "cred": {{ + "setuid": 0, + "setgid": 0, + "caps": [ + "CAP_SYS_ADMIN", + "CAP_SYS_RESOURCE" + ] + }} + }} + ] + }} + ] +}}"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + assert_eq!(config.roles.len(), 1); + assert_eq!(config.roles[0].role, "role1"); + assert_eq!(config.roles[0].tasks.len(), 1); + assert_eq!(config.roles[0].tasks[0].id, IdTask::Name("task1".into())); + assert!(config.options.is_some()); + assert!(config.roles[0].options.is_some()); + assert!(config.roles[0].tasks[0].options.is_some()); + } + + #[test] + fn test_config_optimized_with_options() { + let json = format!( + r#"{{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:05:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep1" + ], + "check": [ + "check1" + ], + "delete": [ + "del1" + ], + "set": {{ + "set1": "value1", + "set2": "value2" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "roles": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:06:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep2" + ], + "check": [ + "check2" + ], + "delete": [ + "del2" + ], + "set": {{ + "set1": "value2", + "set3": "value3" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "name": "role1", + "actors": [ + {{ + "type": "group", + "id": {} + }} + ], + "tasks": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:07:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep3" + ], + "check": [ + "check3" + ], + "delete": [ + "del3" + ], + "set": {{ + "set1": "value3", + "set4": "value4" + }} + }}, + "root": "user", + "bounding": "strict", + "wildcard-denied": ";&|" + }}, + "name": "task1", + "cred": {{ + "setuid": 0, + "setgid": 0, + "caps": [ + "CAP_SYS_ADMIN", + "CAP_SYS_RESOURCE" + ] + }}, + "commands": ["/usr/bin/ls"] + }} + ] + }} + ] +}}"#, + getgid().as_raw() + ); + //convert json to cbor4ii + let cbor = convert_json_to_cbor(&json); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result: Result, _> = deserializer.deserialize( + &mut cbor4ii::serde::Deserializer::new(SliceReader::new(cbor.as_slice())), + ); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + assert_eq!(config.roles.len(), 1); + assert_eq!(config.roles[0].role, "role1"); + assert_eq!(config.roles[0].tasks.len(), 1); + assert_eq!(config.roles[0].tasks[0].id, IdTask::Name("task1".into())); + assert!(config.options.is_some()); + assert!(config.roles[0].options.is_some()); + assert!(config.roles[0].tasks[0].options.is_some()); + assert_eq!(config.roles[0].user_min, ActorMatchMin::GroupMatch(1)); + assert_eq!(config.roles[0].tasks[0].score.cmd_min, CmdMin::Match); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.uid, + Some(SetuidMin::from(&0.into())) + ); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.gid, + Some(SetgidMin::from(&vec![0])) + ); + assert_eq!( + config.roles[0].tasks[0].score.caps_min, + CapsMin::CapsAdmin(2) + ); + assert!(config.roles[0].tasks[0].commands.is_none()); + assert_eq!( + config.roles[0].tasks[0].final_path, + Some(PathBuf::from("/usr/bin/ls")) + ); + } + + #[test] + fn test_optimized_config() { + let uid = getuid().as_raw(); + let json = format!( + r#"{{"roles":[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0"], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]}}"#, + uid + ); + //convert json to cbor4ii + let cbor = convert_json_to_cbor(&json); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder().build(), + }; + let result: Result, _> = deserializer.deserialize( + &mut cbor4ii::serde::Deserializer::new(SliceReader::new(cbor.as_slice())), + ); + assert!(result.is_ok(), "Failed to deserialize: {:?}", result); + let config = result.unwrap(); + assert_eq!(config.roles[0].user_min, ActorMatchMin::UserMatch); + assert_eq!(config.roles[0].tasks[0].score.cmd_min, CmdMin::Match); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.uid, + Some(SetuidMin::from(&0.into())) + ); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.gid, + Some(SetgidMin::from(&vec![0])) + ); + assert_eq!(config.roles[0].tasks[0].score.caps_min, CapsMin::NoCaps); + assert!(config.roles[0].tasks[0].commands.is_none()); + assert_eq!( + config.roles[0].tasks[0].final_path, + Some(PathBuf::from("/usr/bin/ls")) + ); + } + + #[test] + fn test_expecting_error() { + let seq = "[1, 2, 3]"; + let map = "{\"1\": 2, \"3\": 4}"; + let int = "1"; + let float = "1.0"; + let cli = Cli::builder().build(); + let config_finder = ConfigFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + }; + let result = config_finder.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + + let role_list = RoleListFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = role_list.deserialize(&mut serde_json::Deserializer::from_str(map)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let task_list = TaskListFinderDeserializer { + cli: &cli, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = task_list.deserialize(&mut serde_json::Deserializer::from_str(map)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let task = TaskFinderDeserializer { + cli: &cli, + i: 0, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = task.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + assert!(serde_json::from_str::(int).is_err()); + let mut var_name = None; + let mut cmd_min = CmdMin::Match; + let dcommand = DCommandDeserializer { + env_path: &[], + cmd_path: &cli.cmd_path, + cmd_args: &cli.cmd_args, + final_path: &mut var_name, + cmd_min: &mut cmd_min, + }; + let result = dcommand.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let cred = CredFinderDeserializerReturn { cli: &cli }; + let result = cred.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let setuser = SetUserDeserializerReturn { cli: &cli }; + let result = setuser.deserialize(&mut serde_json::Deserializer::from_str(float)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let setgroups = SetGroupsDeserializerReturn { cli: &cli }; + let result = setgroups.deserialize(&mut serde_json::Deserializer::from_str(float)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let actors = ActorsFinderDeserializer { + cred: &Cred::builder().build(), + }; + let result = actors.deserialize(&mut serde_json::Deserializer::from_str(int)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + let role = RoleFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = role.deserialize(&mut serde_json::Deserializer::from_str(int)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + } + + // this test is to check if the deserializer can handle unknown types... It might evolve in the future + #[test] + fn test_unknown_type() { + let json = r#"{"unknown": "unknown"}"#; + let cli = Cli::builder().build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Expected error, got: {:?}", result); + + let deserializer = RoleFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder().build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Expected error, got: {:?}", result); + + let deserializer = TaskFinderDeserializer { + cli: &cli, + i: 0, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Expected error, got: {:?}", result); + + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_err(), "Expected error, got: {:?}", result); + } +} diff --git a/src/sr/finder/mod.rs b/src/sr/finder/mod.rs new file mode 100644 index 00000000..bcc3df6d --- /dev/null +++ b/src/sr/finder/mod.rs @@ -0,0 +1,491 @@ +/// This file implements a finder algorithm within deserialization of the settings +/// It is much more efficient to do it this way, way less memory allocation and manipulation +/// Only the settings that are needed are kept in memory +use std::{ + collections::HashMap, + io::BufReader, + path::{Path, PathBuf}, +}; + +use api::{register_plugins, Api, ApiEvent}; +use capctl::CapSet; +use de::{ConfigFinderDeserializer, DConfigFinder, DLinkedCommand, DLinkedRole, DLinkedTask}; +use log::debug; +use options::BorrowedOptStack; +use rar_common::{ + database::{ + actor::DGroups, + options::{SAuthentication, SBounding, SPrivileged, STimeout}, + score::{CmdMin, Score}, + }, + util::{all_paths_from_env, open_with_privileges}, + Cred, StorageMethod, +}; +use serde::de::DeserializeSeed; + +use crate::Cli; + +mod api; +mod cmd; +mod de; +mod options; + +#[derive(Debug, Default, Clone)] +pub struct BestExecSettings { + pub score: Score, + pub final_path: PathBuf, + pub setuid: Option, + pub setgroups: Option>, + pub caps: Option, + pub task: Option, + pub role: String, + pub env: HashMap, + pub env_path: Vec, + pub bounding: SBounding, + pub timeout: STimeout, + pub auth: SAuthentication, + pub root: SPrivileged, +} + +pub fn find_best_exec_settings<'de: 'a, 'a, P>( + cli: &'a Cli, + cred: &'a Cred, + path: &'a P, + env_vars: impl IntoIterator, impl Into)>, + env_path: &[&str], +) -> Result> +where + P: AsRef, +{ + register_plugins(); + let settings_file = rar_common::get_settings(path)?; + let config_finder_deserializer = ConfigFinderDeserializer { + cli, + cred, + env_path, + }; + match settings_file.storage.method { + StorageMethod::CBOR => { + let file_path = settings_file + .storage + .settings + .unwrap_or_default() + .path + .ok_or("Settings file variable not found")?; + let file = open_with_privileges(&file_path)?; + let reader = BufReader::new(file); // Use BufReader for efficient streaming + let mut io_reader = cbor4ii::core::utils::IoReader::new(reader); // Use IoReader for streaming + Ok(BestExecSettings::retrieve_settings( + cli, + cred, + &config_finder_deserializer + .deserialize(&mut cbor4ii::serde::Deserializer::new(&mut io_reader))?, + env_vars, + &env_path, + )?) + } + StorageMethod::JSON => { + let file_path = settings_file + .storage + .settings + .unwrap_or_default() + .path + .ok_or("Settings file variable not found")?; + let file = open_with_privileges(&file_path)?; + let reader = BufReader::new(file); + let io_reader = serde_json::de::IoRead::new(reader); + Ok(BestExecSettings::retrieve_settings( + cli, + cred, + &config_finder_deserializer + .deserialize(&mut serde_json::Deserializer::new(io_reader))?, + env_vars, + &env_path, + )?) + } + } +} + +impl BestExecSettings { + fn retrieve_settings<'a>( + cli: &'a Cli, + cred: &'a Cred, + data: &'a DConfigFinder<'a>, + env_vars: impl IntoIterator, impl Into)>, + env_path: &[&str], + ) -> Result> { + let mut result = Self::default(); + let mut matching = false; + let mut opt_stack = BorrowedOptStack::new(data.options.clone()); + for role in data.roles() { + matching |= result.role_settings(cli, &role, &mut opt_stack, env_path)?; + } + if !matching { + return Err("No matching role found".into()); + } + result.env = opt_stack + .calc_temp_env(opt_stack.calc_override_behavior(), &cli.opt_filter) + .calc_final_env(env_vars, env_path, cred)?; + result.auth = opt_stack.calc_authentication(); + result.bounding = opt_stack.calc_bounding(); + result.timeout = opt_stack.calc_timeout(); + result.root = opt_stack.calc_privileged(); + Ok(result) + } + + pub fn role_settings<'c, 'a>( + &mut self, + cli: &'c Cli, + data: &DLinkedRole<'c, 'a>, + opt_stack: &mut BorrowedOptStack<'a>, + env_path: &[&str], + ) -> Result> { + debug!("role_settings: {:?}", data.role().role); + if !self.actors_settings(data)? { + return Ok(false); + } + let mut res = false; + for task in data.tasks() { + res |= self.task_settings(cli, &task, opt_stack, env_path)?; + } + Ok(res) + } + + pub fn actors_settings<'c, 'a>( + &mut self, + data: &DLinkedRole<'c, 'a>, + ) -> Result> { + let mut res = !data.role().user_min.is_no_match(); + Api::notify(ApiEvent::ActorMatching(data, self, &mut res))?; + Ok(res) + } + + pub fn task_settings<'t, 'c, 'a>( + &mut self, + cli: &'t Cli, + data: &DLinkedTask<'t, 'c, 'a>, + opt_stack: &mut BorrowedOptStack<'a>, + env_path: &[&str], + ) -> Result> { + debug!("task_settings: {:?}", data.id); + let temp_opt_stack = BorrowedOptStack::from_task(data); + let mut found = false; + let mut f_env_path = None; + if let Some(commands) = data.commands() { + let t_env_path = opt_stack.calc_path(env_path); + for command in commands.del() { + if self.command_settings( + &t_env_path.iter().map(|s| s.as_str()).collect::>(), + cli, + &command, + )? { + return Ok(false); + } + } + if commands.default_behavior.is_some_and(|b| b.is_all()) { + debug!("default behavior is all"); + let t_env_path = opt_stack.calc_path(env_path); + found = true; + debug!("{:?}", &cli.cmd_path); + if let Ok(path) = cli.cmd_path.canonicalize() { + self.final_path = path; + } else { + self.final_path = all_paths_from_env( + &t_env_path.iter().map(|s| s.as_str()).collect::>(), + &cli.cmd_path, + ) + .first() + .ok_or_else::, _>(|| { + "No path found".to_string().into() + })? + .to_path_buf(); + } + self.score.cmd_min = CmdMin::FullWildcardPath | CmdMin::RegexArgs; + } else { + for command in commands.add() { + found = self.command_settings( + &t_env_path.iter().map(|s| s.as_str()).collect::>(), + cli, + &command, + )?; + } + } + f_env_path = Some(t_env_path); + } else if let Some(final_path) = &data.final_path { + debug!("final_path already found: {:?}", final_path); + found = self.update_command_score(final_path.to_path_buf(), data.score.cmd_min); + } + let mut score = data.score(self.score.cmd_min, temp_opt_stack.calc_security_min()); + Api::notify(ApiEvent::BestTaskSettingsFound( + &cli, &data, opt_stack, self, &mut score, + ))?; + if found && score.better_fully(&self.score) { + debug!("found better task settings"); + self.role = data.role().role().role.to_string(); + self.task = Some(data.id.to_string()); + self.env_path = f_env_path + .unwrap_or(opt_stack.calc_path(env_path)) + .iter() + .map(|s| s.to_string()) + .collect(); + self.score = score; + self.setuid = data.setuid.clone().map(|u| u.fetch_id()).flatten(); + self.setgroups = data.setgroups.clone().and_then(|g| match g { + DGroups::Single(g) => Some(vec![g.fetch_id()].into_iter().flatten().collect()), + DGroups::Multiple(g) => Some(g.iter().filter_map(|g| g.fetch_id()).collect()), + }); + self.caps = data.caps.clone(); + opt_stack.set_role(data.role().role().options.clone()); + opt_stack.set_task(data.task().options.clone()); + } + + Ok(found) + } + + pub fn command_settings<'d, 'l, 't, 'c, 'a>( + &mut self, + env_path: &[&str], + cli: &'d Cli, + data: &DLinkedCommand<'d, 'l, 't, 'c, 'a>, + ) -> Result> { + debug!("env_path: {:?}", env_path); + Ok(match &**data { + de::DCommand::Simple(role_cmd) => { + let mut final_path = None; + let cmd_min = cmd::evaluate_command_match( + env_path, + &cli.cmd_path, + &cli.cmd_args, + role_cmd, + &self.score.cmd_min, + &mut final_path, + ); + if let Some(final_path) = final_path { + self.update_command_score(final_path, cmd_min) + } else { + false + } + } + de::DCommand::Complex(value) => { + let mut cmd_min = CmdMin::empty(); + let mut final_path = None; + Api::notify(ApiEvent::ProcessComplexCommand( + value, + env_path, + &cli.cmd_path, + &cli.cmd_args, + &mut cmd_min, + &mut final_path, + ))?; + if let Some(final_path) = final_path { + self.update_command_score(final_path, cmd_min) + } else { + false + } + } + }) + } + + fn update_command_score(&mut self, final_path: PathBuf, res: CmdMin) -> bool { + if res.better(&self.score.cmd_min) { + self.score.cmd_min = res; + self.final_path = final_path; + true + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::de::{DCommand, DCommandList, DRoleFinder, DTaskFinder, IdTask}; + use super::*; + use rar_common::database::score::{ActorMatchMin, CmdMin, Score}; + use rar_common::database::structs::SetBehavior; + use serde_json_borrow::Value; + use std::path::PathBuf; + + use crate::Cli; + use rar_common::Cred; + + // Helper: Dummy implementations for required traits/structs + fn dummy_cli() -> Cli { + Cli::builder() + .cmd_path("/usr/bin/ls".to_string()) + .cmd_args(vec!["-l".to_string()]) + .build() + } + + fn dummy_cred() -> Cred { + Cred::builder().build() + } + + fn dummy_dconfigfinder<'a>() -> DConfigFinder<'a> { + DConfigFinder::builder() + .roles(vec![ + DRoleFinder::builder() + .user_min(ActorMatchMin::UserMatch) + .role("test") + .tasks(vec![ + DTaskFinder::builder() + .id(IdTask::Number(0)) + .caps(!CapSet::empty()) + .commands( + DCommandList::builder(SetBehavior::None) + .add(vec![DCommand::simple("/usr/bin/ls -l")]) + .build(), + ) + .build(), + DTaskFinder::builder() + .id(IdTask::Number(1)) + .caps(CapSet::empty()) + .commands( + DCommandList::builder(SetBehavior::None) + .add(vec![ + DCommand::simple("/usr/bin/ls ^.*$"), + DCommand::complex(Value::Object( + [("key".into(), Value::Str("value".into()))] + .into_iter() + .collect::>() + .into(), + )), + ]) + .build(), + ) + .build(), + ]) + .build(), + DRoleFinder::builder() + .user_min(ActorMatchMin::UserMatch) + .role("test2") + .tasks(vec![ + DTaskFinder::builder() + .id(IdTask::Number(0)) + .caps(!CapSet::empty()) + .commands( + DCommandList::builder(SetBehavior::None) + .add(vec![DCommand::simple("/usr/bin/ls -l")]) + .build(), + ) + .build(), + DTaskFinder::builder() + .id(IdTask::Number(1)) + .caps(CapSet::empty()) + .commands( + DCommandList::builder(SetBehavior::None) + .add(vec![DCommand::simple("/usr/bin/ls ^.*$")]) + .build(), + ) + .build(), + ]) + .build(), + ]) + .build() + } + + #[test] + fn test_retrieve_settings_no_matching_role() { + let cli = dummy_cli(); + let cred = dummy_cred(); + let data = dummy_dconfigfinder(); + let env_vars = vec![("KEY", "VALUE")]; + let env_path = &["/bin"]; + let result = BestExecSettings::retrieve_settings(&cli, &cred, &data, env_vars, env_path); + assert!(result.is_ok()); + } + + #[test] + fn test_role_settings_calls_actors_and_tasks() { + let mut best = BestExecSettings::default(); + let cli = dummy_cli(); + let binding = dummy_dconfigfinder(); + let data = binding.roles().nth(0).unwrap(); + let mut opt_stack = BorrowedOptStack::new(None); + let env_path = &["/bin"]; + let result = best.role_settings(&cli, &data, &mut opt_stack, env_path); + assert!(result.is_ok()); + } + + #[test] + fn test_actors_settings_returns_bool() { + let mut best = BestExecSettings::default(); + let binding = dummy_dconfigfinder(); + let data = binding.roles().nth(0).unwrap(); + let result = best.actors_settings(&data); + assert!(result.is_ok()); + assert!(matches!(result, Ok(_))); + } + + #[test] + fn test_task_settings_sets_fields_on_found() { + let mut best = BestExecSettings::default(); + let cli = dummy_cli(); + let binding = dummy_dconfigfinder(); + let binding = binding.roles().nth(0).unwrap(); + let data = binding.tasks().nth(0).unwrap(); + let mut opt_stack = BorrowedOptStack::new(None); + let env_path = &["/bin"]; + let result = best.task_settings(&cli, &data, &mut opt_stack, env_path); + assert!(result.is_ok()); + } + + #[cfg(feature = "pcre2")] + #[test] + fn test_command_settings_simple_and_complex() { + let mut best = BestExecSettings::default(); + let cli = dummy_cli(); + let env_path = &["/usr/bin"]; + let binding = dummy_dconfigfinder(); + let binding = binding.roles().nth(0).unwrap(); + let binding = binding.tasks().nth(1).unwrap(); + let binding = binding.commands().unwrap(); + let data = binding.add().nth(0).unwrap(); + let result = best.command_settings(env_path, &cli, &data); + assert!(result.is_ok()); + assert!(result.unwrap()); + let data = binding.add().nth(1).unwrap(); + let result = best.command_settings(env_path, &cli, &data); + assert!( + result.is_ok(), + "Failed to process complex command : {}", + result.unwrap_err() + ); + assert!(!result.unwrap()) + } + + #[test] + fn test_update_command_score_better() { + let mut settings = BestExecSettings { + score: Score { + cmd_min: CmdMin::RegexArgs, + ..Default::default() + }, + final_path: PathBuf::from("/old/path"), + ..Default::default() + }; + let new_cmd_min = CmdMin::Match; + let new_path = PathBuf::from("/new/path"); + let updated = settings.update_command_score(new_path.clone(), new_cmd_min.clone()); + assert!(updated); + assert_eq!(settings.score.cmd_min, new_cmd_min); + assert_eq!(settings.final_path, new_path); + } + + #[test] + fn test_update_command_score_not_better() { + let mut settings = BestExecSettings { + score: Score { + cmd_min: CmdMin::Match, + ..Default::default() + }, + final_path: PathBuf::from("/old/path"), + ..Default::default() + }; + let worse_cmd_min = CmdMin::RegexArgs; + let new_path = PathBuf::from("/new/path"); + let updated = settings.update_command_score(new_path, worse_cmd_min); + assert!(!updated); + assert_eq!(settings.final_path, PathBuf::from("/old/path")); + } +} diff --git a/src/sr/finder/options.rs b/src/sr/finder/options.rs new file mode 100644 index 00000000..c3fe0138 --- /dev/null +++ b/src/sr/finder/options.rs @@ -0,0 +1,1256 @@ +use std::collections::HashSet; +use std::error::Error; +use std::{borrow::Cow, collections::HashMap}; + +use bon::{bon, builder, Builder}; +use chrono::Duration; + +use konst::primitive::parse_i64; +use konst::{iter, option, result, slice, string, unwrap_ctx}; +use libc::PATH_MAX; +use rar_common::database::options::{ + EnvBehavior, Level, PathBehavior, SAuthentication, SBounding, SPathOptions, SPrivileged, + STimeout, TimestampType, +}; +use rar_common::database::score::SecurityMin; +use rar_common::database::FilterMatcher; +use std::hash::Hash; + +#[cfg(feature = "pcre2")] +use pcre2::bytes::Regex; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +use log::debug; + +use crate::Cred; + +use super::de::DLinkedTask; + +//#[cfg(feature = "finder")] +//use super::finder::Cred; +//#[cfg(feature = "finder")] +//use super::finder::SecurityMin; + +//=== DPathOptions === + +const ENV_PATH_BEHAVIOR: PathBehavior = result::unwrap_or!( + PathBehavior::try_parse(env!("RAR_PATH_DEFAULT")), + PathBehavior::Delete +); + +const ENV_PATH_ADD_LIST_SLICE: &[&str] = &iter::collect_const!(&str => + string::split(env!("RAR_PATH_ADD_LIST"), ":"), + map(string::trim), +); + +//static ENV_PATH_ADD_LIST: [&str; ENV_PATH_ADD_LIST_SLICE.len()] = *unwrap_ctx!(slice::try_into_array(ENV_PATH_ADD_LIST_SLICE)); + +const ENV_PATH_REMOVE_LIST_SLICE: &[&str] = &iter::collect_const!(&str => + string::split(env!("RAR_PATH_REMOVE_LIST"), ":"), + map(string::trim), +); + +//static ENV_PATH_REMOVE_LIST: [&str; ENV_PATH_REMOVE_LIST_SLICE.len()] = *unwrap_ctx!(slice::try_into_array(ENV_PATH_REMOVE_LIST_SLICE)); + +//=== ENV === +const ENV_DEFAULT_BEHAVIOR: EnvBehavior = result::unwrap_or!( + EnvBehavior::try_parse(env!("RAR_ENV_DEFAULT")), + EnvBehavior::Delete +); + +const ENV_KEEP_LIST_SLICE: &[&str] = &iter::collect_const!(&str => + string::split(env!("RAR_ENV_KEEP_LIST"), ","), + map(string::trim), +); + +const ENV_CHECK_LIST_SLICE: &[&str] = &iter::collect_const!(&str => + string::split(env!("RAR_ENV_CHECK_LIST"), ","), + map(string::trim), +); + +const ENV_DELETE_LIST_SLICE: &[&str] = &iter::collect_const!(&str => + string::split(env!("RAR_ENV_DELETE_LIST"), ","), + map(string::trim), +); + +const ENV_SET_LIST_SLICE: &[(&str, &str)] = &iter::collect_const!((&str, &str) => + string::split(env!("RAR_ENV_SET_LIST"), "\n"), + filter_map(|s| { + if let Some((key,value)) = string::split_once(s, '=') { + Some((string::trim(key),string::trim(value))) + } else { + None + } + }) +); + +const ENV_OVERRIDE_BEHAVIOR: bool = result::unwrap_or!( + konst::primitive::parse_bool(env!("RAR_ENV_OVERRIDE_BEHAVIOR")), + false +); + +static ENV_KEEP_LIST: [&str; ENV_KEEP_LIST_SLICE.len()] = + *unwrap_ctx!(slice::try_into_array(ENV_KEEP_LIST_SLICE)); + +static ENV_CHECK_LIST: [&str; ENV_CHECK_LIST_SLICE.len()] = + *unwrap_ctx!(slice::try_into_array(ENV_CHECK_LIST_SLICE)); + +static ENV_DELETE_LIST: [&str; ENV_DELETE_LIST_SLICE.len()] = + *unwrap_ctx!(slice::try_into_array(ENV_DELETE_LIST_SLICE)); + +static ENV_SET_LIST: [(&str, &str); ENV_SET_LIST_SLICE.len()] = + *unwrap_ctx!(slice::try_into_array(ENV_SET_LIST_SLICE)); + +//=== STimeout === + +const TIMEOUT_TYPE: TimestampType = result::unwrap_or!( + TimestampType::try_parse(env!("RAR_TIMEOUT_TYPE")), + TimestampType::PPID +); + +const TIMEOUT_DURATION: Duration = option::unwrap_or!( + result::unwrap_or!( + convert_string_to_duration(env!("RAR_TIMEOUT_DURATION")), + None + ), + Duration::seconds(5) +); + +const TIMEOUT_MAX_USAGE: u64 = result::unwrap_or!( + konst::primitive::parse_u64(env!("RAR_TIMEOUT_MAX_USAGE")), + 0 +); + +const BOUNDING: SBounding = result::unwrap_or!( + SBounding::try_parse(env!("RAR_BOUNDING")), + SBounding::Strict +); + +const AUTHENTICATION: SAuthentication = result::unwrap_or!( + SAuthentication::try_parse(env!("RAR_AUTHENTICATION")), + SAuthentication::Perform +); + +//const WILDCARD_DENIED: &str = env!("RAR_WILDCARD_DENIED"); + +const PRIVILEGED: SPrivileged = result::unwrap_or!( + SPrivileged::try_parse(env!("RAR_USER_CONSIDERED")), + SPrivileged::User +); + +//#[cfg(not(tarpaulin_include))] +//const fn default() -> Opt<'static> { +/* Opt::builder(Level::Default) +.maybe_root(env!("RAR_USER_CONSIDERED").parse().ok()) +.maybe_bounding(env!("RAR_BOUNDING").parse().ok()) +.path(DPathOptions::default_path()) +.maybe_authentication(env!("RAR_AUTHENTICATION").parse().ok()) +.env( + DEnvOptions::builder( + env!("RAR_ENV_DEFAULT") + .parse() + .unwrap_or(EnvBehavior::Delete), + ) + .keep(env!("RAR_ENV_KEEP_LIST").split(',').collect::>()) + .unwrap() + .check(env!("RAR_ENV_CHECK_LIST").split(',').collect::>()) + .unwrap() + .delete( + env!("RAR_ENV_DELETE_LIST") + .split(',') + .collect::>(), + ) + .unwrap() + .set( + serde_json::from_str(env!("RAR_ENV_SET_LIST")) + .unwrap_or_else(|_| Map::default()) + .into_iter() + .filter_map(|(k, v)| { + if let Some(v) = v.as_str() { + Some((k.to_string(), v.to_string())) + } else { + None + } + }), + ) + .maybe_override_behavior(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().ok()) + .build(), +) +.timeout( + STimeout::builder() + .maybe_type_field(env!("RAR_TIMEOUT_TYPE").parse().ok()) + .maybe_duration( + convert_string_to_duration(&env!("RAR_TIMEOUT_DURATION").to_string()) + .ok() + .flatten(), + ) + .build(), +) +.wildcard_denied(env!("RAR_WILDCARD_DENIED")) +.build() */ +//} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Builder, Default)] +pub struct DPathOptions<'a> { + #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[builder(start_fn)] + pub default_behavior: PathBehavior, + #[serde(borrow, default, skip_serializing_if = "Option::is_none")] + #[builder(with = |v : impl IntoIterator>>| { v.into_iter().map(|s| s.into()).collect() })] + pub add: Option]>>, + #[serde( + borrow, + default, + skip_serializing_if = "Option::is_none", + alias = "del" + )] + #[builder(with = |v : impl IntoIterator>>| { v.into_iter().map(|s| s.into()).collect() })] + pub sub: Option]>>, +} + +#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Default, Builder)] +pub struct DEnvOptions<'a> { + #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[builder(start_fn)] + pub default_behavior: EnvBehavior, + #[serde(alias = "override", default, skip_serializing_if = "Option::is_none")] + pub override_behavior: Option, + #[serde(borrow, default, skip_serializing_if = "HashMap::is_empty")] + #[builder(default, with = |iter: impl IntoIterator>, impl Into>)>| { + let mut map = HashMap::with_hasher(Default::default()); + map.extend(iter.into_iter().map(|(k, v)| (k.into(), v.into()))); + map + })] + pub set: HashMap, Cow<'a, str>>, + #[serde(borrow, default, skip_serializing_if = "HashSet::is_empty")] + #[builder(default, with = |v : impl IntoIterator>>| -> Result<_,Cow<'a,str>> { let mut res = HashSet::new(); for s in v { res.insert(s.into()); } Ok(res)})] + pub keep: HashSet>, + #[serde(borrow, default, skip_serializing_if = "HashSet::is_empty")] + #[builder(default, with = |v : impl IntoIterator>>| -> Result<_,Cow<'a,str>> { let mut res = HashSet::new(); for s in v { res.insert(s.into()); } Ok(res)})] + pub check: HashSet>, + #[serde(borrow, default, skip_serializing_if = "HashSet::is_empty")] + #[builder(default, with = |v : impl IntoIterator>>| -> Result<_,Cow<'a,str>> { let mut res = HashSet::new(); for s in v { res.insert(s.into()); } Ok(res)})] + pub delete: HashSet>, +} + +#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Default)] +#[serde(rename_all = "kebab-case")] +pub struct Opt<'a> { + #[serde(skip)] + pub level: Level, + #[serde(borrow, skip_serializing_if = "Option::is_none")] + pub path: Option>, + #[serde(borrow, skip_serializing_if = "Option::is_none")] + pub env: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub root: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bounding: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub authentication: Option, + #[serde(borrow, skip_serializing_if = "Option::is_none")] + pub wildcard_denied: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout: Option, + #[serde(default, flatten)] + pub _extra_fields: Value, +} + +#[bon] +impl<'a> Opt<'a> { + #[builder] + pub fn new( + #[builder(start_fn)] level: Level, + path: Option>, + env: Option>, + root: Option, + bounding: Option, + authentication: Option, + #[builder(into)] wildcard_denied: Option>, + timeout: Option, + #[builder(default)] _extra_fields: Value, + ) -> Self { + Self { + level, + path, + env, + root, + bounding, + authentication, + wildcard_denied, + timeout, + _extra_fields, + } + } +} + +impl<'a> DEnvOptions<'a> { + pub fn calc_final_env( + &self, + env_vars: impl IntoIterator, impl Into)>, + env_path: &[&str], + target: &Cred, + ) -> Result, Box> { + let mut final_set = match self.default_behavior { + EnvBehavior::Inherit => Err("Internal Error with environment behavior".to_string()), + EnvBehavior::Delete => Ok(env_vars + .into_iter() + .filter_map(|(key, value)| { + let needle = key.into().into(); + let value: String = value.into(); + if env_matches(&self.keep, &needle) + || (env_matches(&self.check, &needle) && check_env(&needle, &value)) + { + Some((needle.to_string(), value)) + } else { + None + } + }) + .collect::>()), + EnvBehavior::Keep => Ok(env_vars + .into_iter() + .filter_map(|(key, value)| { + let needle = key.into().into(); + let value: String = value.into(); + if !env_matches(&self.delete, &needle) + || (env_matches(&self.check, &needle) && check_env(&needle, &value)) + { + Some((needle.to_string(), value)) + } else { + None + } + }) + .collect::>()), + }?; + final_set.insert( + "PATH".into(), + env_path.iter().fold(String::new(), |acc, path| { + if acc.is_empty() { + path.to_string() + } else { + format!("{}:{}", acc, path) + } + }), + ); + final_set.insert("LOGNAME".into(), target.user.name.clone()); + final_set.insert("USER".into(), target.user.name.clone()); + final_set.insert("HOME".into(), target.user.dir.to_string_lossy().to_string()); + final_set + .entry("TERM".into()) + .or_insert_with(|| "unknown".into()); + final_set.insert( + "SHELL".into(), + target.user.shell.to_string_lossy().to_string(), + ); + final_set.extend( + self.set + .iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect::>(), + ); + Ok(final_set) + } +} + +impl Into for Opt<'_> { + fn into(self) -> rar_common::database::options::Opt { + rar_common::database::options::Opt::builder(self.level) + .maybe_path(if let Some(spath) = self.path { + Some( + rar_common::database::options::SPathOptions::builder(spath.default_behavior) + .maybe_add( + spath + .add + .map(|v| v.into_iter().map(|s| s.to_string()).collect::>()), + ) + .maybe_sub( + spath + .sub + .map(|v| v.into_iter().map(|s| s.to_string()).collect::>()), + ) + .build(), + ) + } else { + None + }) + .maybe_env(if let Some(senv) = self.env { + Some( + rar_common::database::options::SEnvOptions::builder(senv.default_behavior) + .maybe_override_behavior(senv.override_behavior) + .set( + senv.set + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(), + ) + .keep( + senv.keep + .into_iter() + .map(|v| v.to_string()) + .collect::>(), + ) + .unwrap() + .check( + senv.check + .into_iter() + .map(|v| v.to_string()) + .collect::>(), + ) + .unwrap() + .delete( + senv.delete + .into_iter() + .map(|v| v.to_string()) + .collect::>(), + ) + .unwrap() + .build(), + ) + } else { + None + }) + .maybe_root(self.root) + .maybe_bounding(self.bounding) + .maybe_authentication(self.authentication) + .maybe_wildcard_denied(self.wildcard_denied) + .maybe_timeout(self.timeout) + .build() + } +} + +impl Into for DPathOptions<'_> { + fn into(self) -> SPathOptions { + SPathOptions::builder(self.default_behavior) + .maybe_add( + self.add + .map(|v| v.into_iter().map(|s| s.to_string()).collect::>()), + ) + .maybe_sub( + self.sub + .map(|v| v.into_iter().map(|s| s.to_string()).collect::>()), + ) + .build() + } +} + +impl DPathOptions<'_> { + pub fn default_path<'a>() -> DPathOptions<'a> { + DPathOptions::builder(ENV_PATH_BEHAVIOR) + .add(ENV_PATH_ADD_LIST_SLICE.iter().map(|p| *p)) + .sub(ENV_PATH_REMOVE_LIST_SLICE.iter().map(|p| *p)) + .build() + } + pub fn calc_path<'a>(&'a self, path_var: &'a [&'a str]) -> Vec<&'a str> { + let default = Default::default(); + match self.default_behavior { + PathBehavior::Inherit | PathBehavior::Delete => { + if let Some(add) = &self.add { + let sub = self.sub.as_ref().unwrap_or(&default); + add.iter() + .filter(|item| !sub.contains(*item)) + .map(|s| s.as_ref()) + .collect() + } else { + Vec::new() + } + } + is_safe => { + let sub = self.sub.as_ref(); + self.add + .as_ref() + .map(|cow| cow.iter()) + .into_iter() + .flatten() + .map(|s| s.as_ref()) + .chain(path_var.iter().copied()) + .filter(move |s| { + let not_in_sub = !sub.is_some_and(|set| set.iter().any(|p| *s == p)); + not_in_sub && (!is_safe.is_keep_safe() || !s.starts_with('/')) + }) + .collect() + } + } + } +} + +impl<'a> DPathOptions<'a> { + pub fn union(&mut self, path_options: DPathOptions<'a>) { + match path_options.default_behavior { + PathBehavior::Inherit => { + if let Some(add) = &path_options.add { + self.add + .get_or_insert_with(Default::default) + .to_mut() + .extend_from_slice(&add); + } + if let Some(sub) = &path_options.sub { + self.sub + .get_or_insert_with(Default::default) + .to_mut() + .extend_from_slice(&sub); + } + } + behaviors => { + self.add = path_options.add.clone(); + self.sub = path_options.sub.clone(); + self.default_behavior = behaviors; + } + } + } +} + +fn check_env(key: impl AsRef, value: impl AsRef) -> bool { + debug!("Checking env: {}={}", key.as_ref(), value.as_ref()); + match key.as_ref() { + "TZ" => tz_is_safe(value.as_ref()), + _ => !value.as_ref().chars().any(|c| c == '/' || c == '%'), + } +} + +fn env_matches(set: &HashSet, needle: &K) -> bool +where + K: AsRef + Eq + Hash, +{ + set.contains(&needle) || set.iter().any(|key| test_pattern(&needle, key.as_ref())) +} + +fn is_valid_env_name(s: &str) -> bool { + let mut chars = s.chars(); + + // Check if the first character is a letter or underscore + if let Some(first_char) = chars.next() { + if !(first_char.is_ascii_alphabetic() || first_char == '_') { + return false; + } + } else { + return false; // Empty string + } + + // Check if the remaining characters are alphanumeric or underscores + chars.all(|c| c.is_ascii_alphanumeric() || c == '_') +} + +#[cfg(feature = "pcre2")] +fn is_regex(s: impl AsRef) -> bool { + Regex::new(s.as_ref()).is_ok() +} + +#[cfg(not(feature = "pcre2"))] +fn is_regex(_s: impl AsRef) -> bool { + false // Always return false if regex feature is disabled +} + +#[cfg(feature = "pcre2")] +fn test_pattern(pattern: impl AsRef, subject: impl AsRef) -> bool { + Regex::new(&format!("^{}$", pattern.as_ref())) // convert to regex + .and_then(|r| r.is_match(subject.as_ref().as_bytes())) + .is_ok_and(|m| m) +} + +#[cfg(not(feature = "pcre2"))] +fn test_pattern(_: impl AsRef, _: impl AsRef) -> bool { + false +} + +fn tz_is_safe(tzval: &str) -> bool { + // tzcode treats a value beginning with a ':' as a path. + let tzval = if let Some(val) = tzval.strip_prefix(':') { + val + } else { + tzval + }; + + // Reject fully-qualified TZ that doesn't begin with the zoneinfo dir. + if tzval.starts_with('/') { + return false; + } + + // Make sure TZ only contains printable non-space characters + // and does not contain a '..' path element. + let mut lastch = '/'; + for cp in tzval.chars() { + if cp.is_ascii_whitespace() || !cp.is_ascii_graphic() { + return false; + } + if lastch == '/' + && cp == '.' + && tzval + .chars() + .nth(tzval.chars().position(|c| c == '.').unwrap() + 1) + == Some('.') + && (tzval + .chars() + .nth(tzval.chars().position(|c| c == '.').unwrap() + 2) + == Some('/') + || tzval + .chars() + .nth(tzval.chars().position(|c| c == '.').unwrap() + 2) + .is_none()) + { + return false; + } + lastch = cp; + } + + // Reject extra long TZ values (even if not a path). + if tzval.len() >= >::try_into(PATH_MAX).unwrap() { + return false; + } + + true +} + +pub fn is_default(t: &T) -> bool { + t == &T::default() +} + +#[derive(Debug)] +struct DurationParseError; +impl std::fmt::Display for DurationParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Invalid duration format") + } +} + +const fn convert_string_to_duration( + s: &str, +) -> Result, DurationParseError> { + let parts = string::split(s, ':'); + let (hours, parts) = match parts.next() { + Some(h) => h, + None => return Err(DurationParseError), + }; + let (minutes, parts) = match parts.next() { + Some(m) => m, + None => return Err(DurationParseError), + }; + let (seconds, _) = match parts.next() { + Some(sec) => sec, + None => return Err(DurationParseError), + }; + + let hours: i64 = unwrap_ctx!(parse_i64(hours)); + let minutes: i64 = unwrap_ctx!(parse_i64(minutes)); + let seconds: i64 = unwrap_ctx!(parse_i64(seconds)); + Ok(Some(Duration::seconds( + hours * 3600 + minutes * 60 + seconds, + ))) +} + +pub struct BorrowedOptStack<'a> { + config: Option>, + role: Option>, + task: Option>, +} + +impl<'a, 'b, 'c, 't> BorrowedOptStack<'a> { + pub fn new(config: Option>) -> Self { + Self { + config, + role: None, + task: None, + } + } + pub fn set_role(&mut self, role: Option>) { + self.role = role; + } + pub fn set_task(&mut self, task: Option>) { + self.task = task; + } + pub fn from_task(task: &DLinkedTask<'t, 'c, 'a>) -> Self { + let config = task.role().config().options.clone(); + let role = task.role().role().options.clone(); + let task_opt = task.task.options.clone(); + Self { + config, + role, + task: task_opt, + } + } + pub fn calc_path(&self, path_var: &[&str]) -> Vec { + // Preallocate with a reasonable guess, but will only allocate once. + let mut combined_paths: Vec = Vec::with_capacity(path_var.len()); + + // Stack of options in order: default, config, role, task + let stack = [ + self.config.as_ref().map(|c| c.path.as_ref()).flatten(), + self.role.as_ref().map(|c| c.path.as_ref()).flatten(), + self.task.as_ref().map(|c| c.path.as_ref()).flatten(), + ]; + + calculate_combined_paths( + path_var, + &mut combined_paths, + &ENV_PATH_BEHAVIOR, + &Some(ENV_PATH_ADD_LIST_SLICE), + &Some(ENV_PATH_REMOVE_LIST_SLICE), + ); + + for opt in stack.iter() { + if let Some(ref path_opt) = opt { + calculate_combined_paths( + path_var, + &mut combined_paths, + &path_opt.default_behavior, + &path_opt.add.as_ref().map(|v| v.into_iter()), + &path_opt.sub.as_ref().map(|v| v.into_iter()), + ); + } + } + combined_paths + } + + pub fn calc_security_min(&self) -> SecurityMin { + let mut security_min = SecurityMin::default(); + [self.task.as_ref(), self.role.as_ref(), self.config.as_ref()] + .iter() + .flatten() + .for_each(|o| { + update_security_min() + .security_min(&mut security_min) + .bounding(&o.bounding) + .root(&o.root) + .authentication(&o.authentication) + .env_behavior(&o.env.as_ref().and_then(|e| Some(e.default_behavior))) + .override_env(&o.env.as_ref().and_then(|e| e.override_behavior)) + .path_behavior(&o.path.as_ref().map(|p| p.default_behavior)) + .call(); + }); + update_security_min() + .security_min(&mut security_min) + .bounding(&Some(BOUNDING)) + .root(&Some(PRIVILEGED)) + .authentication(&Some(AUTHENTICATION)) + .env_behavior(&Some(ENV_DEFAULT_BEHAVIOR)) + .override_env(&Some(ENV_OVERRIDE_BEHAVIOR)) + .path_behavior(&Some(ENV_PATH_BEHAVIOR)) + .call(); + security_min + } + + pub fn calc_override_behavior(&self) -> bool { + [self.task.as_ref(), self.role.as_ref(), self.config.as_ref()] + .iter() + .flatten() + .filter_map(|o| o.env.as_ref()) + .find_map(|o| o.override_behavior) + .unwrap_or(ENV_OVERRIDE_BEHAVIOR) + } + pub fn calc_temp_env( + &self, + override_behavior: bool, + opt_filter: &Option, + ) -> DEnvOptions<'_> { + let mut result = DEnvOptions::default(); + fn determine_final_behavior<'a>( + override_behavior: bool, + opt_filter: &Option, + final_behavior: &mut EnvBehavior, + overriden: &mut bool, + env_behavior: &EnvBehavior, + ) { + if !*overriden { + if let Some(behavior) = opt_filter + .as_ref() + .and_then(|f| { + if override_behavior { + *overriden = true; + f.env_behavior + } else { + None + } + }) + .or_else(|| { + if env_behavior.is_inherit() { + None + } else { + Some(*env_behavior) + } + }) + { + *final_behavior = behavior; + } + } + } + #[builder] + fn assign_env_settings( + override_behavior: bool, + opt_filter: &Option, + result: &mut DEnvOptions<'_>, + overriden: &mut bool, + default_behavior: &EnvBehavior, + keep: &(impl IntoIterator> + Clone), + delete: &(impl IntoIterator> + Clone), + check: &(impl IntoIterator> + Clone), + set: &(impl IntoIterator, impl AsRef)> + Clone), + ) { + determine_final_behavior( + override_behavior, + &opt_filter, + &mut result.default_behavior, + overriden, + &default_behavior, + ); + if default_behavior.is_keep() || default_behavior.is_delete() { + result.set.clear(); + result.keep.clear(); + result.delete.clear(); + result.check.clear(); + } + result.set.extend( + set.clone() + .into_iter() + .filter(|(k, _)| is_valid_env_name(k.as_ref())) + .map(|(k, v)| (k.as_ref().to_string().into(), v.as_ref().to_string().into())), + ); + result.keep.extend( + keep.clone() + .into_iter() + .filter(|p| is_valid_env_name(p.as_ref()) || is_regex(p.as_ref())) + .map(|k| k.as_ref().to_string().into()), + ); + result.delete.extend( + delete + .clone() + .into_iter() + .filter(|p| is_valid_env_name(p.as_ref()) || is_regex(p.as_ref())) + .map(|k| k.as_ref().to_string().into()), + ); + result.check.extend( + check + .clone() + .into_iter() + .filter(|p| is_valid_env_name(p.as_ref()) || is_regex(p.as_ref())) + .map(|k| k.as_ref().to_string().into()), + ); + } + let mut overriden = false; + assign_env_settings() + .override_behavior(override_behavior) + .opt_filter(&opt_filter) + .result(&mut result) + .overriden(&mut overriden) + .default_behavior(&ENV_DEFAULT_BEHAVIOR) + .keep(&ENV_KEEP_LIST) + .check(&ENV_CHECK_LIST) + .delete(&ENV_DELETE_LIST) + .set(&ENV_SET_LIST) + .call(); + [self.config.as_ref(), self.role.as_ref(), self.task.as_ref()] + .iter() + .flatten() + .filter_map(|o| o.env.as_ref()) + .for_each(|o| { + assign_env_settings() + .override_behavior(override_behavior) + .opt_filter(opt_filter) + .result(&mut result) + .overriden(&mut overriden) + .default_behavior(&o.default_behavior) + .keep(&o.keep) + .check(&o.check) + .delete(&o.delete) + .set(&o.set) + .call(); + }); + result + } + + pub fn calc_bounding(&self) -> SBounding { + [self.task.as_ref(), self.role.as_ref(), self.config.as_ref()] + .iter() + .flatten() + .filter_map(|o| o.bounding) + .next() + .unwrap_or(BOUNDING) + } + pub fn calc_timeout(&self) -> STimeout { + [self.task.as_ref(), self.role.as_ref(), self.config.as_ref()] + .iter() + .flatten() + .filter_map(|o| o.timeout.clone()) + .next() + .unwrap_or(STimeout { + type_field: Some(TIMEOUT_TYPE), + duration: Some(TIMEOUT_DURATION), + max_usage: Some(TIMEOUT_MAX_USAGE), + _extra_fields: Map::new(), + }) + } + pub fn calc_authentication(&self) -> SAuthentication { + [self.task.as_ref(), self.role.as_ref(), self.config.as_ref()] + .iter() + .flatten() + .filter_map(|o| o.authentication) + .next() + .unwrap_or(AUTHENTICATION) + } + pub fn calc_privileged(&self) -> SPrivileged { + [self.task.as_ref(), self.role.as_ref(), self.config.as_ref()] + .iter() + .flatten() + .filter_map(|o| o.root) + .next() + .unwrap_or(PRIVILEGED) + } +} + +#[bon::builder] +fn update_security_min( + security_min: &mut SecurityMin, + bounding: &Option, + root: &Option, + authentication: &Option, + env_behavior: &Option, + override_env: &Option, + path_behavior: &Option, +) { + if !security_min.contains(SecurityMin::DisableBounding) + && bounding.is_some_and(|b| b.is_ignore()) + { + *security_min |= SecurityMin::DisableBounding; + } + if !security_min.contains(SecurityMin::EnableRoot) + && root.is_some_and(|r| r == SPrivileged::Privileged) + { + *security_min |= SecurityMin::EnableRoot; + } + if !security_min.contains(SecurityMin::SkipAuth) + && authentication.is_some_and(|a| a == SAuthentication::Skip) + { + *security_min |= SecurityMin::SkipAuth; + } + if !security_min.contains(SecurityMin::KeepEnv) + && env_behavior + .as_ref() + .is_some_and(|e| e.is_keep() || override_env.as_ref().is_some_and(|o| *o)) + { + *security_min |= SecurityMin::KeepEnv; + } + if !security_min.contains(SecurityMin::KeepPath) + && path_behavior.as_ref().is_some_and(|p| p.is_keep_safe()) + { + *security_min |= SecurityMin::KeepPath; + } + if !security_min.contains(SecurityMin::KeepUnsafePath) + && path_behavior.as_ref().is_some_and(|p| p.is_keep_unsafe()) + { + *security_min |= SecurityMin::KeepUnsafePath; + } +} + +fn calculate_combined_paths( + path_var: &[&str], + combined_paths: &mut Vec, + default_behavior: &PathBehavior, + add: &Option + ToString> + Clone>, + sub: &Option + ToString> + Clone>, +) { + match default_behavior { + PathBehavior::Inherit => { + if let Some(ref add_paths) = add { + combined_paths.extend(add_paths.clone().into_iter().map(|p| p.to_string())); + } + if let Some(ref sub_paths) = sub { + // Avoid allocation by using retain and Cow::Borrowed + combined_paths.retain(|path| { + !sub_paths + .clone() + .into_iter() + .any(|p| path.as_str() == p.as_ref()) + }); + } + } + PathBehavior::Delete => { + combined_paths.clear(); + if let Some(ref add_paths) = add { + combined_paths.extend(add_paths.clone().into_iter().map(|p| p.to_string())); + } + } + ref is_safe => { + combined_paths.clear(); + combined_paths.extend( + path_var + .iter() + .map(|s| s.to_string()) + .filter(|path| is_safe.is_keep_unsafe() || path.starts_with('/')), + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tz_is_safe() { + assert!(tz_is_safe("America/New_York")); + assert!(!tz_is_safe("/America/New_York")); + assert!(!tz_is_safe("America/New_York/..")); + //assert path max + assert!(!tz_is_safe( + String::from_utf8(vec![b'a'; (PATH_MAX + 1).try_into().unwrap()]) + .unwrap() + .as_str() + )); + } + + #[test] + fn test_is_valid_env_name() { + assert!(is_valid_env_name("VAR_NAME")); + assert!(is_valid_env_name("_VAR_NAME")); + assert!(!is_valid_env_name("1_VAR_NAME")); + assert!(!is_valid_env_name("VAR-NAME")); + assert!(!is_valid_env_name("VAR NAME")); + assert!(!is_valid_env_name("")); + } + #[test] + fn test_is_regex() { + #[cfg(feature = "pcre2")] + assert!(is_regex("^[a-zA-Z0-9_]+$")); + #[cfg(not(feature = "pcre2"))] + assert!(!is_regex("^[a-zA-Z0-9_]+$")); + assert!(!is_regex("[a-z")); + } + + #[test] + fn test_test_pattern() { + #[cfg(feature = "pcre2")] + assert!(test_pattern("^[a-zA-Z0-9_]+$", "test")); + #[cfg(not(feature = "pcre2"))] + assert!(!test_pattern("^[a-zA-Z0-9_]+$", "test")); + assert!(!test_pattern("[a-z", "test")); + } + + #[test] + fn test_check_env() { + assert!(check_env("TZ", "America/New_York")); + assert!(!check_env("TZ", "/America/New_York")); + assert!(!check_env("TZ", "America/New_York/..")); + assert!(!check_env("VAR_NAME", "VAR%NAME")); + assert!(check_env("VAR_NAME", "VAR_NAME")); + } + + #[test] + fn test_env_matches() { + let set: HashSet = ["VAR1", "VAR2"].iter().map(|s| s.to_string()).collect(); + assert!(env_matches(&set, &"VAR1".to_string())); + assert!(!env_matches(&set, &"VAR3".to_string())); + } + + #[test] + fn test_calc_path() { + let path_options = DPathOptions::builder(PathBehavior::Inherit) + .add(vec!["/usr/local/bin", "/usr/bin"]) + .sub(vec!["/usr/bin"]) + .build(); + let path_var = ["/bin", "/usr/bin"]; + let result = path_options.calc_path(&path_var); + assert_eq!(result, vec!["/usr/local/bin"]); + } + + #[test] + fn test_calc_env() { + let env_options = DEnvOptions::builder(EnvBehavior::Delete) + .set(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")]) + .keep(vec!["VAR3"]) + .unwrap() + .delete(vec!["VAR4"]) + .unwrap() + .check(vec!["VAR5"]) + .unwrap() + .build(); + let env_vars = vec![ + ("VAR1", "AAAA"), + ("VAR3", "VALUE3"), + ("VAR4", "VALUE4"), + ("VAR5", "VALUE5"), + ]; + let env_path = vec!["/usr/local/bin", "/usr/bin"]; + let target = Cred::builder().build(); + let result = env_options.calc_final_env(env_vars, &env_path, &target); + assert!( + result.is_ok(), + "Failed to calculate final env {}", + result.unwrap_err() + ); + let final_env = result.unwrap(); + assert_eq!(final_env.get("PATH").unwrap(), "/usr/local/bin:/usr/bin"); + assert_eq!(*final_env.get("LOGNAME").unwrap(), target.user.name); + assert_eq!(*final_env.get("USER").unwrap(), target.user.name); + assert_eq!( + *final_env.get("HOME").unwrap(), + target.user.dir.to_string_lossy() + ); + assert_eq!(final_env.get("TERM").unwrap(), "unknown"); + assert_eq!( + *final_env.get("SHELL").unwrap(), + target.user.shell.to_string_lossy() + ); + assert_eq!(final_env.get("VAR1").unwrap(), "VALUE1"); + assert_eq!(final_env.get("VAR2").unwrap(), "VALUE2"); + assert_eq!(final_env.get("VAR3").unwrap(), "VALUE3"); + assert!(final_env.get("VAR4").is_none()); + assert!(final_env.get("VAR5").unwrap() == "VALUE5"); + + let env_options = DEnvOptions::builder(EnvBehavior::Keep) + .set(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")]) + .keep(vec!["VAR3"]) + .unwrap() + .delete(vec!["VAR4"]) + .unwrap() + .check(vec!["VAR5"]) + .unwrap() + .build(); + let env_vars = vec![ + ("VAR1", "AAAA"), + ("VAR3", "VALUE3"), + ("VAR4", "VALUE4"), + ("VAR5", "VALUE5"), + ]; + let env_path = vec!["/usr/local/bin", "/usr/bin"]; + let target = Cred::builder().build(); + let result = env_options.calc_final_env(env_vars, &env_path, &target); + assert!( + result.is_ok(), + "Failed to calculate final env {}", + result.unwrap_err() + ); + let final_env = result.unwrap(); + assert_eq!(final_env.get("PATH").unwrap(), "/usr/local/bin:/usr/bin"); + assert_eq!(*final_env.get("LOGNAME").unwrap(), target.user.name); + assert_eq!(*final_env.get("USER").unwrap(), target.user.name); + assert_eq!( + *final_env.get("HOME").unwrap(), + target.user.dir.to_string_lossy() + ); + assert_eq!(final_env.get("TERM").unwrap(), "unknown"); + assert_eq!( + *final_env.get("SHELL").unwrap(), + target.user.shell.to_string_lossy() + ); + assert_eq!(final_env.get("VAR1").unwrap(), "VALUE1"); + assert_eq!(final_env.get("VAR2").unwrap(), "VALUE2"); + assert_eq!(final_env.get("VAR3").unwrap(), "VALUE3"); + assert!(final_env.get("VAR4").is_none()); + assert!(final_env.get("VAR5").unwrap() == "VALUE5"); + + let env_options = DEnvOptions::builder(EnvBehavior::Inherit) + .set(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")]) + .keep(vec!["VAR3"]) + .unwrap() + .delete(vec!["VAR4"]) + .unwrap() + .check(vec!["VAR5"]) + .unwrap() + .build(); + let env_vars = vec![ + ("VAR1", "AAAA"), + ("VAR3", "VALUE3"), + ("VAR4", "VALUE4"), + ("VAR5", "VALUE5"), + ]; + let env_path = vec!["/usr/local/bin", "/usr/bin"]; + let target = Cred::builder().build(); + let result = env_options.calc_final_env(env_vars, &env_path, &target); + assert!(result.is_err()); + } + + #[test] + fn test_is_default() { + let default = Opt::default(); + assert!(is_default(&default)); + let non_default = Opt::builder(Level::Default).build(); + assert!(!is_default(&non_default)); + } + + #[test] + fn test_convert_string_to_duration() { + let duration = convert_string_to_duration(&"01:30:00".to_string()); + assert!(duration.is_ok()); + assert_eq!( + duration.unwrap(), + Some(Duration::hours(1) + Duration::minutes(30)) + ); + let invalid_duration = convert_string_to_duration(&"invalid".to_string()); + assert!(invalid_duration.is_err()); + } + + #[test] + fn test_borrowed_opt_stack() { + let config = Some( + Opt::builder(Level::Global) + .env( + DEnvOptions::builder(EnvBehavior::Delete) + .check(["CHECKME"]) + .unwrap() + .set([("VAR1", "VALUE1"), ("VAR2", "VALUE2")]) + .build(), + ) + .build(), + ); + let role = Some( + Opt::builder(Level::Role) + .env( + DEnvOptions::builder(EnvBehavior::Inherit) + .delete(["DELETEME"]) + .unwrap() + .build(), + ) + .build(), + ); + let task = Some( + Opt::builder(Level::Task) + .env( + DEnvOptions::builder(EnvBehavior::Inherit) + .keep(["KEEPME"]) + .unwrap() + .build(), + ) + .build(), + ); + let mut stack = BorrowedOptStack::new(config); + stack.set_role(role); + stack.set_task(task); + assert_eq!( + stack.calc_path(&["/test"]), + env!("RAR_PATH_ADD_LIST").split(':').collect::>() + ); + let env = stack.calc_temp_env(false, &None); + assert_eq!(env.delete, HashSet::from(["DELETEME".into()])); + assert_eq!(env.keep, HashSet::from(["KEEPME".into()])); + assert_eq!(env.check, HashSet::from(["CHECKME".into()])); + assert_eq!( + env.set, + HashMap::from([ + ("VAR1".into(), "VALUE1".into()), + ("VAR2".into(), "VALUE2".into()) + ]) + ); + } + + #[test] + fn test_opt_into_opt() { + let opt = Opt::builder(Level::Default) + .path( + DPathOptions::builder(PathBehavior::Inherit) + .add(["/usr/local/bin"]) + .build(), + ) + .env( + DEnvOptions::builder(EnvBehavior::Keep) + .set([("VAR1", "VALUE1")]) + .build(), + ) + .build(); + let rar_opt: rar_common::database::options::Opt = opt.clone().into(); + assert_eq!(rar_opt.level, Level::Default); + assert_eq!( + rar_opt.path.unwrap().default_behavior, + PathBehavior::Inherit + ); + assert_eq!(rar_opt.env.unwrap().default_behavior, EnvBehavior::Keep); + } +} diff --git a/src/sr/main.rs b/src/sr/main.rs index de9a8e40..dd266dbe 100644 --- a/src/sr/main.rs +++ b/src/sr/main.rs @@ -1,40 +1,34 @@ +mod finder; pub mod pam; mod timeout; +use bon::Builder; use capctl::CapState; use const_format::formatcp; -use nix::{ - libc::dev_t, - sys::stat, - unistd::{getgroups, getuid, isatty, Group, User}, -}; -use rar_common::database::{ - actor::{SGroupType, SGroups, SUserType}, - finder::{Cred, TaskMatch, TaskMatcher}, - options::EnvBehavior, - FilterMatcher, -}; -use rar_common::database::{options::OptStack, structs::SConfig}; +use finder::BestExecSettings; +use nix::{sys::stat, unistd::isatty}; use rar_common::util::escape_parser_string; +use rar_common::{ + database::{ + actor::{SGroupType, SGroups, SUserType}, + options::EnvBehavior, + FilterMatcher, + }, + Cred, +}; use log::{debug, error}; use pam::PAM_PROMPT; use pty_process::blocking::{Command, Pty}; -use std::{cell::RefCell, error::Error, io::stdout, os::fd::AsRawFd, rc::Rc}; +use std::{error::Error, io::stdout, os::fd::AsRawFd, path::PathBuf}; -use rar_common::plugin::register_plugins; -use rar_common::{ - self, - database::read_json_config, - util::{ - activates_no_new_privs, dac_override_effective, drop_effective, read_effective, - setgid_effective, setpcap_effective, setuid_effective, subsribe, BOLD, RST, UNDERLINE, - }, - Storage, +use rar_common::util::{ + activates_no_new_privs, drop_effective, setgid_effective, setpcap_effective, setuid_effective, + subsribe, BOLD, RST, UNDERLINE, }; #[cfg(not(test))] -const ROOTASROLE: &str = "/etc/security/rootasrole.json"; +const ROOTASROLE: &str = env!("RAR_CFG_PATH"); #[cfg(test)] const ROOTASROLE: &str = "target/rootasrole.json"; @@ -59,7 +53,7 @@ const USAGE: &str = formatcp!( {BOLD}-t, --task {RST} Task option allows you to select a specific task to use in the selected role. Note: You must specify a role to designate a task - {BOLD}-E, --preserve-env {RST} + {BOLD}-E, --preserve-env{RST} Preserve environment variables if allowed by a matching task {BOLD}-p, --prompt {RST} @@ -69,7 +63,7 @@ const USAGE: &str = formatcp!( {BOLD}-u, --user {RST} Specify the user to execute the command as - {BOLD} -g --group {RST} + {BOLD}-g --group (,...){RST} Specify the group to execute the command as {BOLD}-i, --info{RST} @@ -82,37 +76,39 @@ const USAGE: &str = formatcp!( RST = RST ); -#[derive(Debug)] +#[derive(Debug, Builder)] struct Cli { /// Role option allows you to select a specific role to use. opt_filter: Option, + #[builder(into)] /// Prompt option allows you to override the default password prompt and use a custom one. - prompt: String, + prompt: Option, + #[builder(default, with = || true)] /// Display rights of executor info: bool, + #[builder(default, with = || true)] /// Display help help: bool, - /// Command to execute - command: Vec, + #[builder(default, into)] + /// A non-absolute path to the command that needs to be found in the PATH + cmd_path: PathBuf, + #[builder(default, with = |i : impl IntoIterator> | i.into_iter().map(|s| s.into()).collect())] + /// Command arguments + cmd_args: Vec, + + #[builder(default, with = || true)] /// Use stdin for password prompt stdin: bool, } impl Default for Cli { fn default() -> Self { - Cli { - opt_filter: None, - prompt: PAM_PROMPT.to_string(), - info: false, - help: false, - stdin: false, - command: vec![], - } + Cli::builder().prompt(PAM_PROMPT).build() } } @@ -125,16 +121,6 @@ fn cap_effective_error(caplist: &str) -> String { ) } -fn from_json_execution_settings( - args: &Cli, - config: &Rc>, - user: &Cred, -) -> Result> { - config - .matches(user, &args.opt_filter, &args.command) - .map_err(|m| m.into()) -} - fn getopt(s: I) -> Result> where I: IntoIterator, @@ -180,10 +166,11 @@ where env.replace(EnvBehavior::Keep); } "-p" | "--prompt" => { - args.prompt = iter - .next() - .map(|s| escape_parser_string(s)) - .unwrap_or_default(); + args.prompt = Some( + iter.next() + .map(|s| escape_parser_string(s)) + .expect("Missing prompt for -p option"), + ); } "-i" | "--info" => { args.info = true; @@ -195,7 +182,7 @@ where if arg.as_ref().starts_with('-') { return Err(format!("Unknown option: {}", arg.as_ref()).into()); } else { - args.command.push(escape_parser_string(arg)); + args.cmd_path = arg.as_ref().into(); break; } } @@ -206,23 +193,25 @@ where .maybe_role(role) .maybe_task(task) .maybe_env_behavior(env) - .maybe_user(user) - .maybe_group(group) + .maybe_user(user)? + .maybe_group(group)? .build(), ); for arg in iter { - args.command.push(escape_parser_string(arg)); + args.cmd_args.push(escape_parser_string(arg)); } Ok(args) } #[cfg(not(tarpaulin_include))] fn main() -> Result<(), Box> { + use std::env; + use crate::{pam::check_auth, ROOTASROLE}; + use finder::find_best_exec_settings; subsribe("sr")?; drop_effective()?; - register_plugins(); let args = std::env::args(); if args.len() < 2 { println!("{}", USAGE); @@ -234,47 +223,39 @@ fn main() -> Result<(), Box> { println!("{}", USAGE); return Ok(()); } - read_effective(true) - .or(dac_override_effective(true)) - .unwrap_or_else(|_| panic!("{}", cap_effective_error("dac_read_search or dac_override"))); - let settings = rar_common::get_settings(ROOTASROLE).expect("Failed to get settings"); - read_effective(false) - .and(dac_override_effective(false)) - .unwrap_or_else(|_| panic!("{}", cap_effective_error("dac_read"))); - let config = match settings.clone().as_ref().borrow().storage.method { - rar_common::StorageMethod::JSON => { - Storage::JSON(read_json_config(settings, ROOTASROLE).expect("Failed to read config")) - } - _ => { - return Err("Unsupported storage method".into()); - } - }; let user = make_cred(); - let taskmatch = match config { - Storage::JSON(ref config) => from_json_execution_settings(&args, config, &user) - .inspect_err(|e| { - error!("{}", e); - }) - .unwrap_or_default(), - }; - let execcfg = &taskmatch.settings; - - let optstack = &execcfg.opt; - check_auth(optstack, &config, &user, &args.prompt)?; - - if !taskmatch.fully_matching() { + let execcfg = find_best_exec_settings( + &args, + &user, + &ROOTASROLE.to_string(), + env::vars(), + env::var("PATH") + .unwrap_or_default() + .split(':') + .collect::>() + .as_slice(), + )?; + + check_auth( + &execcfg.auth, + &execcfg.timeout, + &user, + &args.prompt.unwrap_or(PAM_PROMPT.to_string()), + )?; + + if !execcfg.score.fully_matching() { println!("You are not allowed to execute this command, this incident will be reported."); error!( - "User {} tried to execute command : {:?} without the permission.", - &user.user.name, args.command + "User {} tried to execute command : {:?} {:?} without the permission.", + &user.user.name, args.cmd_path, args.cmd_args ); std::process::exit(1); } if args.info { - println!("Role: {}", execcfg.role().as_ref().borrow().name); - println!("Task: {}", execcfg.task().as_ref().borrow().name); + //println!("Role: {}", if execcfg.role.is_empty() { "None" } else { &execcfg.role }); + //println!("Task: {}", execcfg.task); println!( "With capabilities: {}", execcfg @@ -287,33 +268,27 @@ fn main() -> Result<(), Box> { } // disable root - if !optstack.get_root_behavior().1.is_privileged() { + if execcfg.root.is_user() { activates_no_new_privs().expect("Failed to activate no new privs"); } debug!("setuid : {:?}", execcfg.setuid); - setuid_setgid(execcfg); - let cred = make_cred(); - - set_capabilities(execcfg, optstack); + setuid_setgid(&execcfg); - //execute command - let envset = optstack - .calculate_filtered_env(args.opt_filter, cred, std::env::vars()) - .expect("Failed to calculate env"); + set_capabilities(&execcfg); let pty = Pty::new().expect("Failed to create pty"); debug!( "Command: {:?} {:?}", - execcfg.exec_path, - execcfg.exec_args.join(" ") + execcfg.final_path, + args.cmd_args.join(" ") ); - let command = Command::new(&execcfg.exec_path) - .args(execcfg.exec_args.iter()) + let command = Command::new(&execcfg.final_path) + .args(args.cmd_args.iter()) .env_clear() - .envs(envset) + .envs(execcfg.env) .stdin(std::process::Stdio::inherit()) .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) @@ -322,7 +297,7 @@ fn main() -> Result<(), Box> { Ok(command) => command, Err(e) => { error!("{}", e); - eprintln!("sr: {} : {}", execcfg.exec_path.display(), e); + eprintln!("sr: {} : {}", execcfg.final_path.display(), e); std::process::exit(1); } }; @@ -331,45 +306,18 @@ fn main() -> Result<(), Box> { } fn make_cred() -> Cred { - let user = User::from_uid(getuid()) - .expect("Failed to get user") - .expect("Failed to get user"); - let mut groups = getgroups() - .expect("Failed to get groups") - .iter() - .map(|g| { - Group::from_gid(*g) - .expect("Failed to get group") - .expect("Failed to get group") - }) - .collect::>(); - groups.insert( - 0, - Group::from_gid(user.gid) - .expect("Failed to get group") - .expect("Failed to get group"), - ); - debug!("User: {} ({}), Groups: {:?}", user.name, user.uid, groups,); - let mut tty: Option = None; - if let Ok(stat) = stat::fstat(stdout().as_raw_fd()) { - if let Ok(istty) = isatty(stdout().as_raw_fd()) { - if istty { - tty = Some(stat.st_rdev); + return Cred::builder() + .maybe_tty(stat::fstat(stdout().as_raw_fd()).ok().and_then(|s| { + if isatty(stdout().as_raw_fd()).ok().unwrap_or(false) { + Some(s.st_rdev) + } else { + None } - } - } - // get parent pid - let ppid = nix::unistd::getppid(); - - Cred { - user, - groups, - tty, - ppid, - } + })) + .build(); } -fn set_capabilities(execcfg: &rar_common::database::finder::ExecSettings, optstack: &OptStack) { +fn set_capabilities(execcfg: &BestExecSettings) { //set capabilities if let Some(caps) = execcfg.caps { // case where capabilities are more than bounding set @@ -379,7 +327,7 @@ fn set_capabilities(execcfg: &rar_common::database::finder::ExecSettings, optsta } setpcap_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setpcap"))); let mut capstate = CapState::empty(); - if !optstack.get_bounding().1.is_ignore() { + if execcfg.bounding.is_strict() { for cap in (!caps).iter() { capctl::bounding::drop(cap).expect("Failed to set bounding cap"); } @@ -394,158 +342,66 @@ fn set_capabilities(execcfg: &rar_common::database::finder::ExecSettings, optsta 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() { + if execcfg.bounding.is_strict() { capctl::bounding::clear().expect("Failed to clear bounding cap"); } + capctl::ambient::clear().expect("Failed to clear ambient 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: &rar_common::database::finder::ExecSettings) { - let uid = execcfg.setuid.as_ref().and_then(|u| { - let res = u.fetch_user(); - if let Some(user) = res { - Some(user.uid.as_raw()) - } else { - None - } - }); - let gid = execcfg.setgroups.as_ref().and_then(|g| match g { - SGroups::Single(g) => { - let res = g.fetch_group(); - if let Some(group) = res { - Some(group.gid.as_raw()) - } else { - None - } - } - SGroups::Multiple(g) => { - let res = g.first().unwrap().fetch_group(); - if let Some(group) = res { - Some(group.gid.as_raw()) - } else { - None - } - } - }); - let groups = execcfg.setgroups.as_ref().and_then(|g| match g { - SGroups::Single(g) => { - let res = g.fetch_group(); - if let Some(group) = res { - Some(vec![group.gid.as_raw()]) - } else { - None - } - } - SGroups::Multiple(g) => { - let res = g.iter().map(|g| g.fetch_group()); - let mut groups = Vec::new(); - for group in res.flatten() { - groups.push(group.gid.as_raw()); - } - Some(groups) - } - }); +fn setuid_setgid(execcfg: &BestExecSettings) { + let gid = execcfg.setgroups.as_ref().and_then(|g| g.first().cloned()); setgid_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setgid"))); setuid_effective(true).unwrap_or_else(|_| panic!("{}", cap_effective_error("setuid"))); - capctl::cap_set_ids(uid, gid, groups.as_deref()).expect("Failed to set ids"); + capctl::cap_set_ids(execcfg.setuid, gid, execcfg.setgroups.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"))); } #[cfg(test)] mod tests { + use capctl::{Cap, CapSet}; use libc::getgid; - use nix::unistd::Pid; - use rar_common::database::actor::SActor; - use rar_common::rc_refcell; + use nix::unistd::{getuid, Pid}; + use rar_common::database::options::SBounding; use super::*; - use rar_common::database::make_weak_config; - use rar_common::database::structs::{IdTask, SCommand, SCommands, SConfig, SRole, STask}; - - #[test] - fn test_from_json_execution_settings() { - let mut args = Cli { - opt_filter: None, - prompt: PAM_PROMPT.to_string(), - info: false, - help: false, - stdin: 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::user(0).build()); - 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.opt_filter = Some(FilterMatcher::default()); - args.opt_filter.as_mut().unwrap().role = Some("role1".to_owned()); - let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); - assert!(taskmatch.fully_matching()); - args.opt_filter.as_mut().unwrap().task = Some("task1".to_owned()); - let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); - assert!(taskmatch.fully_matching()); - args.opt_filter.as_mut().unwrap().task = Some("task2".to_owned()); - let taskmatch = from_json_execution_settings(&args, &config, &user).unwrap(); - assert!(taskmatch.fully_matching()); - args.opt_filter.as_mut().unwrap().task = Some("task3".to_owned()); - let taskmatch = from_json_execution_settings(&args, &config, &user); - assert!(taskmatch.is_err()); - args.opt_filter.as_mut().unwrap().role = None; - let taskmatch = from_json_execution_settings(&args, &config, &user); - assert!(taskmatch.is_err()); - } #[test] fn test_getopt() { let args = getopt(vec![ - "chsr", "-r", "role1", "-t", "task1", "-p", "prompt", "-i", "-h", "ls", "-l", + "sr", + "-u", + "root", + "-g", + "root,root", + "-r", + "role1", + "-t", + "task1", + "-p", + "prompt", + "-E", + "-i", + "-h", + "ls", + "-l", ]) .unwrap(); let opt_filter = args.opt_filter.as_ref().unwrap(); + assert_eq!(opt_filter.user, Some(0)); + assert_eq!(opt_filter.group, Some(vec![0, 0])); assert_eq!(opt_filter.role.as_deref(), Some("role1")); assert_eq!(opt_filter.task.as_deref(), Some("task1")); - assert_eq!(args.prompt, "prompt"); + assert_eq!(args.prompt.unwrap(), "prompt"); assert!(args.info); assert!(args.help); - assert_eq!(args.command, vec!["ls".to_string(), "-l".to_string()]); + assert_eq!(args.cmd_path, PathBuf::from("ls")); + assert_eq!(args.cmd_args, vec!["-l".to_string()]); } #[test] @@ -558,4 +414,73 @@ mod tests { assert_eq!(user.groups[0].gid.as_raw(), gid); assert_eq!(user.ppid, Pid::parent()); } + + #[test] + fn test_setuid_setgid() { + let mut capset = CapState::get_current().unwrap(); + if capset.permitted.has(Cap::SETUID) && capset.permitted.has(Cap::SETGID) { + println!("setuid and setgid are available"); + capset.effective.add(Cap::SETUID); + capset.effective.add(Cap::SETGID); + capset.set_current().unwrap(); + let mut execcfg = BestExecSettings::default(); + execcfg.setuid = Some(1000); + execcfg.setgroups = Some(vec![1000]); + setuid_setgid(&execcfg); + assert_eq!(getuid().as_raw(), execcfg.setuid.unwrap()); + if let Some(gid) = execcfg.setgroups.as_ref().and_then(|g| g.first()) { + assert_eq!(unsafe { getgid() }, *gid); + } + capset.effective.clear(); + capset.set_current().unwrap(); + } + } + + #[test] + fn test_set_capabilities() { + let mut capset = CapState::get_current().unwrap(); + if capset.permitted.has(Cap::SETPCAP) + && capset.permitted.has(Cap::SETUID) + && capset.permitted.has(Cap::SETGID) + { + capset.effective.add(Cap::SETPCAP); + capset.set_current().unwrap(); + let mut execcfg = BestExecSettings::default(); + let mut capset = CapSet::empty(); + capset.add(Cap::SETUID); + capset.add(Cap::SETGID); + capset.add(Cap::SETPCAP); + execcfg.caps = Some(capset); + set_capabilities(&execcfg); + let capset = CapState::get_current().unwrap(); + assert!(capset.permitted.has(Cap::SETUID)); + assert!(capset.permitted.has(Cap::SETGID)); + assert!(capset.permitted.has(Cap::SETPCAP)); + assert!(capset.inheritable.has(Cap::SETUID)); + assert!(capset.inheritable.has(Cap::SETGID)); + assert!(capset.inheritable.has(Cap::SETPCAP)); + assert!(capctl::bounding::probe().has(Cap::SETUID)); + assert!(capctl::bounding::probe().has(Cap::SETGID)); + assert!(capctl::bounding::probe().has(Cap::SETPCAP)); + assert!(capctl::ambient::probe().unwrap().has(Cap::SETUID)); + assert!(capctl::ambient::probe().unwrap().has(Cap::SETGID)); + assert!(capctl::ambient::probe().unwrap().has(Cap::SETPCAP)); + execcfg.caps = None; + execcfg.bounding = SBounding::Strict; + set_capabilities(&execcfg); + let capset = CapState::get_current().unwrap(); + assert!(!capset.permitted.has(Cap::SETUID)); + assert!(!capset.permitted.has(Cap::SETGID)); + assert!(!capset.permitted.has(Cap::SETPCAP)); + assert!(!capset.inheritable.has(Cap::SETUID)); + assert!(!capset.inheritable.has(Cap::SETGID)); + assert!(!capset.inheritable.has(Cap::SETPCAP)); + assert!(!capctl::bounding::probe().has(Cap::SETUID)); + assert!(!capctl::bounding::probe().has(Cap::SETGID)); + assert!(!capctl::bounding::probe().has(Cap::SETPCAP)); + assert!(!capctl::ambient::probe().unwrap().has(Cap::SETUID)); + assert!(!capctl::ambient::probe().unwrap().has(Cap::SETGID)); + assert!(!capctl::ambient::probe().unwrap().has(Cap::SETPCAP)); + } + } } diff --git a/src/sr/pam/mod.rs b/src/sr/pam/mod.rs index 9a6b913c..1a914d50 100644 --- a/src/sr/pam/mod.rs +++ b/src/sr/pam/mod.rs @@ -10,8 +10,8 @@ use pcre2::bytes::RegexBuilder; use crate::timeout; use rar_common::{ - database::{finder::Cred, options::OptStack}, - Storage, + database::options::{SAuthentication, STimeout}, + Cred, }; use self::rpassword::Terminal; @@ -120,19 +120,16 @@ impl ConversationHandler for SrConversationHandler { } pub(super) fn check_auth( - optstack: &OptStack, - config: &Storage, + authentication: &SAuthentication, + timeout: &STimeout, user: &Cred, prompt: &str, ) -> Result<(), Box> { - if optstack.get_authentication().1.is_skip() { + if authentication.is_skip() { warn!("Skipping authentication, this is a security risk!"); return Ok(()); } - let timeout = optstack.get_timeout().1; - let is_valid = match config { - Storage::JSON(_) => timeout::is_valid(user, user, &timeout), - }; + let is_valid = timeout::is_valid(user, user, &timeout); debug!("need to re-authenticate : {}", !is_valid); if !is_valid { let conv = SrConversationHandler::new(prompt); @@ -141,10 +138,6 @@ pub(super) fn check_auth( context.authenticate(Flag::SILENT)?; context.acct_mgmt(Flag::SILENT)?; } - match config { - Storage::JSON(_) => { - timeout::update_cookie(user, user, &timeout)?; - } - } + timeout::update_cookie(user, user, &timeout)?; Ok(()) } diff --git a/src/sr/timeout.rs b/src/sr/timeout.rs index 2f36782b..5408eab8 100644 --- a/src/sr/timeout.rs +++ b/src/sr/timeout.rs @@ -16,14 +16,12 @@ use nix::{ use serde::{Deserialize, Serialize}; use rar_common::{ - database::{ - finder::Cred, - options::{STimeout, TimestampType}, - }, + database::options::{STimeout, TimestampType}, util::{ create_dir_all_with_privileges, create_with_privileges, open_with_privileges, remove_with_privileges, }, + Cred, }; /// This module checks the validity of a user's credentials @@ -176,7 +174,7 @@ fn read_cookies(user: &Cred) -> Result, Box> { write_lockfile(&lockpath); let mut file = open_with_privileges(&path)?; let reader = BufReader::new(&mut file); - let res = ciborium::de::from_reader::, BufReader<_>>(reader)?; + let res = cbor4ii::serde::from_reader::, BufReader<_>>(reader)?; Ok(res) } @@ -188,7 +186,7 @@ fn save_cookies(user: &Cred, cookies: &[CookieVersion]) -> Result<(), Box io::Result<()> { let config = BufReader::new(File::open(ROOTASROLE)?); let mut config: SettingsFile = serde_json::from_reader(config)?; - // Get the filesystem type - if let Some(fs_type) = get_filesystem_type(ROOTASROLE)? { - match fs_type.as_str() { - "ext2" | "ext3" | "ext4" | "xfs" | "btrfs" | "ocfs2" | "jfs" | "reiserfs" => { - info!( - "{} is compatble for immutability, setting immutable flag", + if env!("RAR_CFG_IMMUTABLE") == "true" { + // Get the filesystem type + if let Some(fs_type) = get_filesystem_type(ROOTASROLE)? { + match fs_type.as_str() { + "ext2" | "ext3" | "ext4" | "xfs" | "btrfs" | "ocfs2" | "jfs" | "reiserfs" => { + info!( + "{} is compatble for immutability, setting immutable flag", + fs_type + ); + set_immutable(&mut config, true); + toggle_lock_config(&ROOTASROLE.to_string(), ImmutableLock::Set)?; + return Ok(()); + } + _ => info!( + "{} is not compatible for immutability, removing immutable flag", fs_type - ); - set_immutable(&mut config, true); - toggle_lock_config(&ROOTASROLE.to_string(), ImmutableLock::Set)?; - return Ok(()); + ), } - _ => info!( - "{} is not compatible for immutability, removing immutable flag", - fs_type - ), + } else { + info!("Failed to get filesystem type, removing immutable flag"); } - } else { - info!("Failed to get filesystem type, removing immutable flag"); } + set_immutable(&mut config, false); File::create(ROOTASROLE)?.write_all(serde_json::to_string_pretty(&config)?.as_bytes())?; Ok(()) } +fn set_options(content: &mut String) -> io::Result<()> { + let mut config: SettingsFile = serde_json::from_str(content)?; + config.storage.method = env!("RAR_CFG_TYPE").parse().unwrap(); + if let Some(settings) = &mut config.storage.settings { + if let Some(path) = &mut settings.path { + *path = env!("RAR_CFG_DATA_PATH").to_string(); + } + if let Some(immutable) = &mut settings.immutable { + *immutable = env!("RAR_CFG_IMMUTABLE").parse().unwrap(); + } + } + config.storage.options = Some(Opt { + timeout: Some(STimeout { + type_field: Some(env!("RAR_TIMEOUT_TYPE").parse().unwrap()), + duration: convert_string_to_duration(&env!("RAR_TIMEOUT_DURATION").to_string()) + .unwrap(), + max_usage: if env!("RAR_TIMEOUT_MAX_USAGE").len() > 0 { + Some(env!("RAR_TIMEOUT_MAX_USAGE").parse().unwrap()) + } else { + None + }, + _extra_fields: Value::Null, + }), + path: Some(SPathOptions { + default_behavior: env!("RAR_PATH_DEFAULT").parse().unwrap(), + add: Some( + env!("RAR_PATH_ADD_LIST") + .split(":") + .map(|s| s.to_string()) + .collect(), + ), + sub: if env!("RAR_PATH_REMOVE_LIST").len() > 0 { + Some( + env!("RAR_PATH_REMOVE_LIST") + .split(":") + .map(|s| s.to_string()) + .collect(), + ) + } else { + None + }, + _extra_fields: Value::Null, + }), + env: Some(SEnvOptions { + default_behavior: env!("RAR_ENV_DEFAULT").parse().unwrap(), + override_behavior: if env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().unwrap() { + Some(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().unwrap()) + } else { + None + }, + keep: Some( + env!("RAR_ENV_KEEP_LIST") + .split(",") + .map(|s| s.to_string()) + .collect(), + ), + check: Some( + env!("RAR_ENV_CHECK_LIST") + .split(",") + .map(|s| s.to_string()) + .collect(), + ), + delete: Some( + env!("RAR_ENV_DELETE_LIST") + .split(",") + .map(|s| s.to_string()) + .collect(), + ), + set: if env!("RAR_ENV_SET_LIST").len() > 0 && env!("RAR_ENV_SET_LIST") != "{}" { + serde_json::from_str(env!("RAR_ENV_SET_LIST")).unwrap() + } else { + HashMap::new() + }, + _extra_fields: Value::Null, + }), + root: Some(env!("RAR_USER_CONSIDERED").parse().unwrap()), + bounding: Some(env!("RAR_BOUNDING").parse().unwrap()), + wildcard_denied: Some(env!("RAR_WILDCARD_DENIED").to_string()), + authentication: Some(env!("RAR_AUTHENTICATION").parse().unwrap()), + _extra_fields: Value::Null, + }); + *content = serde_json::to_string_pretty(&config)?; + Ok(()) +} + fn set_immutable(config: &mut SettingsFile, value: bool) { if let Some(settings) = config.storage.settings.as_mut() { if let Some(mut _immutable) = settings.immutable { @@ -161,9 +253,14 @@ fn deploy_config_file() -> Result { let mut status = ConfigState::Unchanged; // Check if the target file exists if !Path::new(ROOTASROLE).exists() { - info!("Config file does not exist, deploying default file"); + info!( + "Config file {} does not exist, deploying default file", + ROOTASROLE + ); // If the target file does not exist, copy the default file + cap_effective(Cap::DAC_OVERRIDE, true).context("Failed to raise DAC_OVERRIDE")?; deploy_config(ROOTASROLE)?; + cap_effective(Cap::DAC_OVERRIDE, false).context("Failed to raise DAC_OVERRIDE")?; } else { status = config_state()?; } @@ -214,6 +311,8 @@ fn deploy_config>(config_path: P) -> Result<(), anyhow::Error> { content = content.replace("\"ROOTADMINISTRATOR\"", &format!("{}", getuid().as_raw())); } } + // deploy execution options on the config file defined in the compilation environment variables + set_options(&mut content)?; // Write the config file let mut config = File::create(config_path)?; config.write_all(content.as_bytes())?; diff --git a/xtask/src/installer/install.rs b/xtask/src/installer/install.rs index 072cfaac..64e28a7b 100644 --- a/xtask/src/installer/install.rs +++ b/xtask/src/installer/install.rs @@ -16,10 +16,12 @@ use crate::installer::Profile; use crate::util::{change_dir_to_git_root, detect_priv_bin, BOLD, RED, RST}; use anyhow::{anyhow, Context}; -use super::{CHSR_DEST, SR_DEST}; +use super::{CHSR_DEST, RAR_BIN_PATH, SR_DEST}; use crate::util::cap_clear; fn copy_executables(profile: &Profile) -> Result<(), anyhow::Error> { + let chsr_dest = Path::new(RAR_BIN_PATH).join(CHSR_DEST); + let sr_dest = Path::new(RAR_BIN_PATH).join(SR_DEST); let binding = std::env::current_dir()?; let cwd = binding .to_str() @@ -27,7 +29,10 @@ fn copy_executables(profile: &Profile) -> Result<(), anyhow::Error> { info!("Current working directory: {}", cwd); info!( "Copying files {}/target/{}/sr to {} and {}", - cwd, profile, SR_DEST, CHSR_DEST + cwd, + profile, + sr_dest.to_str().unwrap(), + chsr_dest.to_str().unwrap() ); let s_sr = format!("{}/target/{}/sr", cwd, profile); let sr = Path::new(&s_sr); @@ -44,9 +49,9 @@ fn copy_executables(profile: &Profile) -> Result<(), anyhow::Error> { debug!("Copying chsr to chsr.tmp"); fs::copy(chsr, format!("{}.tmp", s_chsr))?; debug!("Renaming sr to /usr/bin/sr"); - fs::rename(sr, SR_DEST)?; + fs::rename(sr, sr_dest)?; debug!("Renaming chsr to /usr/bin/chsr"); - fs::rename(chsr, CHSR_DEST)?; + fs::rename(chsr, chsr_dest)?; debug!("Renaming sr.tmp to sr"); fs::rename(format!("{}.tmp", s_sr), sr)?; debug!("Renaming chsr.tmp to chsr"); @@ -118,8 +123,10 @@ fn copy_docs() -> Result<(), anyhow::Error> { } fn chmod() -> Result<(), anyhow::Error> { - let sr_file = File::open(SR_DEST)?; - let chsr_file = File::open(CHSR_DEST)?; + let chsr_dest = Path::new(RAR_BIN_PATH).join(CHSR_DEST); + let sr_dest = Path::new(RAR_BIN_PATH).join(SR_DEST); + let sr_file = File::open(sr_dest)?; + let chsr_file = File::open(chsr_dest)?; let mode = Mode::from_bits(0o555).expect("Invalid mode bits"); fchmod(sr_file.as_raw_fd(), mode)?; fchmod(chsr_file.as_raw_fd(), mode)?; @@ -129,17 +136,20 @@ fn chmod() -> Result<(), anyhow::Error> { } fn chown() -> Result<(), anyhow::Error> { + let chsr_dest = Path::new(RAR_BIN_PATH).join(CHSR_DEST); + let sr_dest = Path::new(RAR_BIN_PATH).join(SR_DEST); let uid_owner = Uid::from_raw(0); let gid_owner = Gid::from_raw(0); - nix::unistd::chown(SR_DEST, Some(uid_owner), Some(gid_owner))?; - nix::unistd::chown(CHSR_DEST, Some(uid_owner), Some(gid_owner))?; + nix::unistd::chown(&sr_dest, Some(uid_owner), Some(gid_owner))?; + nix::unistd::chown(&chsr_dest, Some(uid_owner), Some(gid_owner))?; Ok(()) } fn setfcap() -> Result<(), anyhow::Error> { + let sr_dest = Path::new(RAR_BIN_PATH).join(SR_DEST); let mut file_caps = capctl::caps::FileCaps::empty(); file_caps.permitted = !CapSet::empty(); - file_caps.set_for_file(SR_DEST)?; + file_caps.set_for_file(sr_dest)?; Ok(()) } @@ -202,6 +212,7 @@ pub fn install( env::set_var("ROOTASROLE_INSTALLER_NESTED", "1"); log::warn!("Elevating privileges..."); std::process::Command::new(priv_exe) + .arg("-E") .arg( current_exe()? .to_str() diff --git a/xtask/src/installer/mod.rs b/xtask/src/installer/mod.rs index 135b64fe..df24954b 100644 --- a/xtask/src/installer/mod.rs +++ b/xtask/src/installer/mod.rs @@ -18,9 +18,9 @@ use crate::{ configure, util::{detect_priv_bin, get_os, OsTarget}, }; - -pub const SR_DEST: &str = "/usr/bin/sr"; -pub const CHSR_DEST: &str = "/usr/bin/chsr"; +pub const RAR_BIN_PATH: &str = env!("RAR_BIN_PATH"); +pub const SR_DEST: &str = "sr"; +pub const CHSR_DEST: &str = "chsr"; #[derive(Debug, Parser, Clone)] pub struct InstallOptions { diff --git a/xtask/src/util.rs b/xtask/src/util.rs index 0452386a..0c2086a8 100644 --- a/xtask/src/util.rs +++ b/xtask/src/util.rs @@ -1,4 +1,6 @@ use std::{ + collections::HashMap, + error::Error, fs::{self, File}, io, os::{fd::AsRawFd, unix::fs::MetadataExt}, @@ -9,12 +11,13 @@ use std::{ use anyhow::{anyhow, Context}; use capctl::Cap; use capctl::CapState; +use chrono::Duration; use clap::ValueEnum; use log::debug; use nix::libc::{FS_IOC_GETFLAGS, FS_IOC_SETFLAGS}; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Serialize}; use serde_json::Value; -use strum::{Display, EnumIs, EnumIter}; +use strum::{Display, EnumIs, EnumIter, EnumString}; #[derive(Debug, Clone, ValueEnum, EnumIs, EnumIter, Display, PartialEq, Eq, Hash)] #[clap(rename_all = "lowercase")] @@ -65,10 +68,27 @@ pub struct SettingsFile { pub _extra_fields: Value, } +#[derive(Serialize, Deserialize, Debug, Clone, EnumString)] +#[strum(ascii_case_insensitive)] +#[serde(rename_all = "lowercase")] +pub enum StorageMethod { + JSON, + CBOR, + // SQLite, + // PostgreSQL, + // MySQL, + // LDAP, + #[serde(other)] + Unknown, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Settings { + pub method: StorageMethod, #[serde(skip_serializing_if = "Option::is_none")] pub settings: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub options: Option, #[serde(default)] #[serde(flatten)] pub _extra_fields: Value, @@ -78,13 +98,163 @@ pub struct Settings { pub struct RemoteStorageSettings { #[serde(skip_serializing_if = "Option::is_none")] pub immutable: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(default)] + #[serde(flatten)] + pub _extra_fields: Value, +} + +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum PathBehavior { + Delete, + KeepSafe, + KeepUnsafe, + #[default] + Inherit, +} + +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display, EnumString, +)] +#[strum(ascii_case_insensitive)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum TimestampType { + #[default] + PPID, + TTY, + UID, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)] +pub struct STimeout { + #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")] + pub type_field: Option, + #[serde( + serialize_with = "serialize_duration", + deserialize_with = "deserialize_duration", + skip_serializing_if = "Option::is_none" + )] + pub duration: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_usage: Option, #[serde(default)] #[serde(flatten)] pub _extra_fields: Value, } +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct SPathOptions { + #[serde(rename = "default", default, skip_serializing_if = "is_default")] + pub default_behavior: PathBehavior, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub add: Option>, + #[serde(default, skip_serializing_if = "Option::is_none", alias = "del")] + pub sub: Option>, + #[serde(default)] + #[serde(flatten)] + pub _extra_fields: Value, +} + +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum EnvBehavior { + Delete, + Keep, + #[default] + Inherit, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)] +pub struct SEnvOptions { + #[serde(rename = "default", default, skip_serializing_if = "is_default")] + pub default_behavior: EnvBehavior, + #[serde(alias = "override", default, skip_serializing_if = "Option::is_none")] + pub override_behavior: Option, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub set: HashMap, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub keep: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub check: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub delete: Option>, + #[serde(default, flatten)] + pub _extra_fields: Value, +} + +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum SBounding { + Strict, + Ignore, + #[default] + Inherit, +} + +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] +#[serde(rename_all = "kebab-case")] +#[derive(Default)] +pub enum SPrivileged { + Privileged, + #[default] + User, + Inherit, +} + +#[derive( + Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, +)] +#[strum(ascii_case_insensitive)] +#[serde(rename_all = "kebab-case")] +#[derive(Default)] +pub enum SAuthentication { + Skip, + #[default] + Perform, + Inherit, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Opt { + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub env: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub root: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bounding: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub authentication: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub wildcard_denied: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout: Option, + #[serde(default, flatten)] + pub _extra_fields: Value, +} + const FS_IMMUTABLE_FL: u32 = 0x00000010; -pub const ROOTASROLE: &str = "/etc/security/rootasrole.json"; +pub const ROOTASROLE: &str = env!("RAR_CFG_PATH"); #[derive(Debug, EnumIs)] pub enum ImmutableLock { @@ -92,6 +262,52 @@ pub enum ImmutableLock { Unset, } +pub fn is_default(t: &T) -> bool { + t == &T::default() +} + +fn serialize_duration(value: &Option, serializer: S) -> Result +where + S: serde::Serializer, +{ + // hh:mm:ss format + match value { + Some(value) => serializer.serialize_str(&format!( + "{:#02}:{:#02}:{:#02}", + value.num_hours(), + value.num_minutes() % 60, + value.num_seconds() % 60 + )), + None => serializer.serialize_none(), + } +} + +fn deserialize_duration<'de, D>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + match convert_string_to_duration(&s) { + Ok(d) => Ok(d), + Err(e) => Err(de::Error::custom(e)), + } +} + +pub fn convert_string_to_duration(s: &String) -> Result, Box> { + let mut parts = s.split(':'); + //unwrap or error + if let (Some(hours), Some(minutes), Some(seconds)) = (parts.next(), parts.next(), parts.next()) + { + let hours: i64 = hours.parse()?; + let minutes: i64 = minutes.parse()?; + let seconds: i64 = seconds.parse()?; + return Ok(Some( + Duration::hours(hours) + Duration::minutes(minutes) + Duration::seconds(seconds), + )); + } + Err("Invalid duration format".into()) +} + fn immutable_required_privileges(file: &File, effective: bool) -> Result<(), capctl::Error> { //get file owner let metadata = file.metadata().unwrap();