diff --git a/Makefile b/Makefile index 1e4de334..726323df 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,8 @@ build: $(BINS) install: build cp -f $(BINS) /usr/bin + chown root:root /usr/bin/sr /usr/bin/chsr /usr/bin/capable + chmod 0555 /usr/bin/sr /usr/bin/chsr /usr/bin/capable setcap "=p" /usr/bin/sr setcap cap_dac_override,cap_sys_admin,cap_sys_ptrace+ep /usr/bin/capable diff --git a/book/src/README.md b/book/src/README.md index 8e6d92d7..69714dbe 100644 --- a/book/src/README.md +++ b/book/src/README.md @@ -1,19 +1,33 @@ # Introduction -**RootAsRole** is a prject to allow Linux/Unix administrators to delegate their administrative tasks access rights to users. This tool allows you to configure your privilege access management more securely on a single operating system. +**RootAsRole** is a project to allow Linux/Unix administrators to delegate their administrative tasks access rights to users. Its main features are : -Unlike sudo, this project sets the principle least privilege on its core features. Like sudo, this project wants to be usable. More than sudo, we care about configurators, and we try to warn configurators about dangerous manipulations. - -By using a role-based access control model, this project allows us to better manage administrative tasks. With this project, you could distribute privileges and prevent them from escalating directly. Unlike sudo does, we don't want to give entire privileges for any insignificant administrative task. You can configure our tool easily with `chsr` command. To find out which capability is needed for a administrative command, we provide the `capable` command. With these two tools, administrators could respect the least privilege principle on their system. - -What we offer that sudo don't : -* [Linux Capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) support * [A structured access control model based on Roles](https://dl.acm.org/doi/10.1145/501978.501980) -* Command matching based on commonly-used open-source libraries + * [Role hierarchy](https://dl.acm.org/doi/10.1145/501978.501980) + * [Static/Dynamic Separation of Duties](https://dl.acm.org/doi/10.1145/501978.501980) +* [Linux Capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html) support, to minimize the privileges of the user executing the command. + * Prevent the escalation of privileges via Bounding set manipulation. +* [Highly configurable](chsr/README.md) with a simple command line interface. This interface is designed to be as easy as `ip` command. + * 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. +* Command matching based on commonly-used open-source libraries: * [glob](https://docs.rs/glob/latest/glob/) for binary path * [PCRE2](https://www.pcre.org/) for command arguments -* Standardized file configuration with [JSON](https://www.json.org/) -* Separation of duties. -* Multi-layered configuration. -* A simple and easy-to-use configuration command line interface. + +## Usage + +The main command line tool is `sr`. It allows you to execute a command by simply typing: + +```bash +sr +``` + +You can find more information about this command in the [sr](sr/README.md) section. + +The `chsr` command allows you to configure the roles and capabilities of the system. You can find more information about this command in the [Configure RootAsRole](chsr/README.md) section. + +## Comparison with sudo + +By using a role-based access control model, this project allows us to better manage administrative tasks. With this project, you could distribute privileges and prevent them from escalating directly. Unlike sudo does, we don't want to give entire privileges for any insignificant administrative task. You can configure our tool easily with `chsr` command. To find out which capability is needed for a administrative command, we provide the `capable` command. With these two tools, administrators could configure its system to respect the least privilege principle. diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 4a54aae6..8cb4e5ac 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -5,7 +5,9 @@ # User Guide - [Installation](guide/installation.md) -- [Configure RootAsRole](chsr/README.md) +- [`sr` Command Line Tool](sr/README.md) +- [`chsr` Command Line Tool](chsr/README.md) + # Knowledge Guide @@ -18,9 +20,7 @@ # Reference Guide -- [`sr` Command Line Tool](sr/README.md) -- [`chsr` Command Line Tool](chsr/README.md) - +- [Configure RootAsRole](chsr/file-config.md) - [Continuous Integration](continuous-integration.md) - [How to contribute](dev/CONTRIBUTE.md) - [Code of Conduct](dev/CODE_OF_CONDUCT.md) diff --git a/book/src/chsr/README.md b/book/src/chsr/README.md index 962134a0..f7a6ca46 100644 --- a/book/src/chsr/README.md +++ b/book/src/chsr/README.md @@ -1,342 +1,10 @@ # Chsr tool documentation -Chsr is a command-line tool to configure roles, permissions and execution options. - -## How does configuration work? - -The configuration is stored in a JSON file. The default path is `/etc/security/rootasrole.json`. It is possible to change the path where the configuration is stored by changing the `path` setting in the configuration file manually. -Note: The configuration file must be immutable after edition. -```json -"storage": { - "method": "json", - "settings": { - "path": "/etc/security/rootasrole.json", - "immutable": true - } -} -``` - -Next, the configuration is divided into roles, tasks, commands, credentials, and options. Each role can have multiple tasks, each task can have multiple commands and credentials. The options are global and can be set for the whole configuration or for a specific role or task. - -## How options work with examples - -### Path options example 1 - -Here is an example global configuration: - -```json -{ - "options": { - "path": { - "default": "delete-all", - "add": [ - "/usr/bin" - ] - } - } -} -``` - -This configuration will delete all paths and add `/usr/bin` to the whitelist. - -```json -{ - "options": { - "path": { - "default": "delete-all", - "add": [ - "/usr/bin" - ] - } - }, - "roles": { - "admin": { - "options": { - "path": { - "default": "inherit", - "add": [ - "/usr/sbin" - ] - } - } - } - } -} -``` - -This configuration will delete all paths and add `/usr/bin` to the whitelist for all roles. The `admin` role will inherit the global configuration and add `/usr/sbin` to the whitelist. So the final configuration for the `admin` role will be `/usr/bin:/usr/sbin`. - -### Path options example 2 - -Here is an example global configuration: - -```json -{ - "options": { - "path": { - "default": "keep-safe", - "add": [ - "/usr/bin" - ] - } - } -} -``` - -This configuration will keep all paths that are absolute and add `/usr/bin` to the path. - -```json -{ - "options": { - "path": { - "default": "keep-safe", - "add": [ - "/usr/bin" - ] - } - }, - "roles": { - "admin": { - "options": { - "path": { - "default": "inherit", - "add": [ - "/usr/sbin" - ] - } - } - } - } -} -``` - -This configuration will keep all paths that are absolute and add `/usr/bin` to the whitelist for all roles. The `admin` role will inherit the global configuration and add `/usr/sbin` to the whitelist. So the final configuration for the `admin` role will be `/usr/bin:/usr/sbin:$PATH`, where `$PATH` is the current executor path value. - -### Path options example 3 - -Here is an example global configuration: - -```json -{ - "options": { - "path": { - "default": "keep-unsafe", - "sub": [ - "/usr/bin" - ] - } - } -} -``` - -This configuration will keep all paths, even them that are relative, and remove `/usr/bin` from the path. - -```json -{ - "options": { - "path": { - "default": "keep-unsafe", - "add": [ - "/usr/bin" - ] - } - }, - "roles": { - "admin": { - "options": { - "path": { - "default": "inherit", - "add": [ - "/usr/sbin" - ] - } - } - } - } -} -``` - -This configuration will keep all paths, even them that are relative, and add `/usr/bin` to the whitelist for all roles. The `admin` role will inherit the global configuration and add `/usr/sbin` to the whitelist. So the final configuration for the `admin` role will be `/usr/bin:/usr/sbin:$PATH`, where `$PATH` is the current executor path value. - -Note: path are always prepended to the current path value. - -### Path options example 4 - -Here is an example global configuration: - -```json -{ - "options": { - "path": { - "default": "inherit", - "add": [ - "/usr/bin" - ] - } - } -} -``` - -If the policy is inherit in global configuration, the policy will be `delete-all`. - -```json -{ - "options": { - "path": { - "default": "delete-all", - "add": [ - "/usr/bin" - ] - } - }, - "roles": { - "admin": { - "options": { - "path": { - "default": "keep-safe", - "sub": [ - "/usr/sbin" - ] - } - }, - "tasks": { - "task1": { - "options": { - "path": { - "default": "inherit", - "add": [ - "/usr/sbin" - ] - } - } - } - } - } - } -} -``` - -This complex configuration will delete-all paths in the global configuration for all roles except for `admin` role. The `admin` role will keep all paths that are absolute and remove `/usr/sbin` from the path. The `task1` task will inherit the `admin` role configuration and tries to add `/usr/sbin` to the path but it will be ignored because the task inherits the `admin` role configuration, and it removes `/usr/sbin` from the path. So the final path is the current executor path value less `/usr/sbin`. - -In conclusion, two logical properties can be deducted : -1. The path removed from the path variable cannot be added, even by inheritance. -2. When a more precise configuration defines a policy (delete-all,keep-safe,keep-unsafe), it will override less precise configuration. - * Global is less precise than Role, Role is less precise than Task - -### Environment options example 1 - -Here is an example global configuration: - -```json -{ - "options": { - "env": { - "default": "delete", - "keep": [ - "VAR1" - ] - } - } -} -``` - -Environment variables are managed in the same way as paths. The policy can be `delete`, `keep`, or `inherit`. The `delete` policy will remove all environment variables except the ones in the `keep` list. The `keep` list is a list of environment variables that will be kept in the environment. - -```json -{ - "options": { - "env": { - "default": "delete", - "keep": [ - "VAR1" - ] - } - }, - "roles": { - "admin": { - "options": { - "env": { - "default": "inherit", - "keep": [ - "VAR2" - ] - } - } - } - } -} -``` - -This configuration will delete all environment variables except `VAR1` for all roles. The `admin` role will inherit the global configuration and keep `VAR2` in the environment. So only `VAR1` and `VAR2` values will be kept in the environment for the `admin` role. - -### Environment options example 2 - -Here is an example global configuration: - -```json -{ - "options": { - "env": { - "default": "keep", - "delete": [ - "VAR1" - ] - } - } -} -``` - -The `delete` list is a list of environment variables that will be removed from the environment. - -```json -{ - "options": { - "env": { - "policy": "keep", - "delete": [ - "VAR1" - ] - } - }, - "roles": { - "admin": { - "options": { - "env": { - "policy": "inherit", - "delete": [ - "VAR2" - ] - } - } - } - } -} -``` - -This configuration will keep all environment variables except `VAR1` for all roles. The `admin` role will inherit the global configuration and remove `VAR2` from the environment. So only `VAR1` and `VAR2` values are removed from the environment for the `admin` role. - -### Environment options example 3 - -Here is an example global configuration: - -```json -{ - "options": { - "env": { - "default": "keep", - "check": [ - "VAR1" - ] - } - } -} -``` - -The `check` list is a list of environment variables that will be checked for unsafe characters. If an environment variable contains unsafe characters, it will be removed from the environment. +Chsr is a command-line tool to configure roles, permissions and execution options. If you want to know how the file configuration works, you can check the [file configuration](file-config.md) section. ## Usage -
+
 Usage: chsr [command] [options]
 
 Commands:
@@ -355,7 +23,10 @@ chsr role [role_name] [operation] [options]
   grant                         Grant permissions to a user or group.
   revoke                        Revoke permissions from a user or group.
     -u, --user [user_name]      Specify a user for grant or revoke operations.
-    -g, --group [group_names]   Specify one or more groups combinaison for grant or revoke operations.
+    -g, --group [nameA,...]     Specify one or more groups combinaison for grant or revoke operations.
+Example : chsr role roleA grant -u userA -g groupA,groupB -g groupC
+This command will grant roleA to "userA", "users that are in groupA AND groupB" and "groupC".
+
 
 
 Task Operations:
@@ -378,6 +49,8 @@ chsr role [role_name] task [task_name] command [cmd]
 chsr role [role_name] task [task_name] credentials [operation]
   show                          Show credentials.
   set, unset                    Set or unset credentials details.
+     --setuid [user]            Specify the user to set.
+     --setgid [group,...]       Specify groups to set.
   caps                          Manage capabilities for credentials.
 
 
@@ -429,4 +102,4 @@ 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.
-
\ No newline at end of file +
diff --git a/book/src/chsr/file-config.md b/book/src/chsr/file-config.md new file mode 100644 index 00000000..76bd3571 --- /dev/null +++ b/book/src/chsr/file-config.md @@ -0,0 +1,337 @@ +# How does configuration work? + +The configuration is stored in a JSON file. The default path is `/etc/security/rootasrole.json`. It is possible to change the path where the configuration is stored by changing the `path` setting in the configuration file manually. +Note: The configuration file must be immutable after edition. +```json +"storage": { + "method": "json", + "settings": { + "path": "/etc/security/rootasrole.json", + "immutable": true + } +} +``` + +Next, the configuration is divided into roles, tasks, commands, credentials, and options. Each role can have multiple tasks, each task can have multiple commands and credentials. The options are global and can be set for the whole configuration or for a specific role or task. + +## How configuration work with examples + +### Role example + + + +## How options work with examples + +### Path options example 1 + +Here is an example global configuration: + +```json +{ + "options": { + "path": { + "default": "delete-all", + "add": [ + "/usr/bin" + ] + } + } +} +``` + +This configuration will delete all paths and add `/usr/bin` to the whitelist. + +```json +{ + "options": { + "path": { + "default": "delete-all", + "add": [ + "/usr/bin" + ] + } + }, + "roles": { + "admin": { + "options": { + "path": { + "default": "inherit", + "add": [ + "/usr/sbin" + ] + } + } + } + } +} +``` + +This configuration will delete all paths and add `/usr/bin` to the whitelist for all roles. The `admin` role will inherit the global configuration and add `/usr/sbin` to the whitelist. So the final configuration for the `admin` role will be `/usr/bin:/usr/sbin`. + +### Path options example 2 + +Here is an example global configuration: + +```json +{ + "options": { + "path": { + "default": "keep-safe", + "add": [ + "/usr/bin" + ] + } + } +} +``` + +This configuration will keep all paths that are absolute and add `/usr/bin` to the path. + +```json +{ + "options": { + "path": { + "default": "keep-safe", + "add": [ + "/usr/bin" + ] + } + }, + "roles": { + "admin": { + "options": { + "path": { + "default": "inherit", + "add": [ + "/usr/sbin" + ] + } + } + } + } +} +``` + +This configuration will keep all paths that are absolute and add `/usr/bin` to the whitelist for all roles. The `admin` role will inherit the global configuration and add `/usr/sbin` to the whitelist. So the final configuration for the `admin` role will be `/usr/bin:/usr/sbin:$PATH`, where `$PATH` is the current executor path value. + +### Path options example 3 + +Here is an example global configuration: + +```json +{ + "options": { + "path": { + "default": "keep-unsafe", + "sub": [ + "/usr/bin" + ] + } + } +} +``` + +This configuration will keep all paths, even them that are relative, and remove `/usr/bin` from the path. + +```json +{ + "options": { + "path": { + "default": "keep-unsafe", + "add": [ + "/usr/bin" + ] + } + }, + "roles": { + "admin": { + "options": { + "path": { + "default": "inherit", + "add": [ + "/usr/sbin" + ] + } + } + } + } +} +``` + +This configuration will keep all paths, even them that are relative, and add `/usr/bin` to the whitelist for all roles. The `admin` role will inherit the global configuration and add `/usr/sbin` to the whitelist. So the final configuration for the `admin` role will be `/usr/bin:/usr/sbin:$PATH`, where `$PATH` is the current executor path value. + +Note: path are always prepended to the current path value. + +### Path options example 4 + +Here is an example global configuration: + +```json +{ + "options": { + "path": { + "default": "inherit", + "add": [ + "/usr/bin" + ] + } + } +} +``` + +If the policy is inherit in global configuration, the policy will be `delete-all`. + +```json +{ + "options": { + "path": { + "default": "delete-all", + "add": [ + "/usr/bin" + ] + } + }, + "roles": { + "admin": { + "options": { + "path": { + "default": "keep-safe", + "sub": [ + "/usr/sbin" + ] + } + }, + "tasks": { + "task1": { + "options": { + "path": { + "default": "inherit", + "add": [ + "/usr/sbin" + ] + } + } + } + } + } + } +} +``` + +This complex configuration will delete-all paths in the global configuration for all roles except for `admin` role. The `admin` role will keep all paths that are absolute and remove `/usr/sbin` from the path. The `task1` task will inherit the `admin` role configuration and tries to add `/usr/sbin` to the path but it will be ignored because the task inherits the `admin` role configuration, and it removes `/usr/sbin` from the path. So the final path is the current executor path value less `/usr/sbin`. + +In conclusion, two logical properties can be deducted : +1. The path removed from the path variable cannot be added, even by inheritance. +2. When a more precise configuration defines a policy (delete-all,keep-safe,keep-unsafe), it will override less precise configuration. + * Global is less precise than Role, Role is less precise than Task + +### Environment options example 1 + +Here is an example global configuration: + +```json +{ + "options": { + "env": { + "default": "delete", + "keep": [ + "VAR1" + ] + } + } +} +``` + +Environment variables are managed in the same way as paths. The policy can be `delete`, `keep`, or `inherit`. The `delete` policy will remove all environment variables except the ones in the `keep` list. The `keep` list is a list of environment variables that will be kept in the environment. + +```json +{ + "options": { + "env": { + "default": "delete", + "keep": [ + "VAR1" + ] + } + }, + "roles": { + "admin": { + "options": { + "env": { + "default": "inherit", + "keep": [ + "VAR2" + ] + } + } + } + } +} +``` + +This configuration will delete all environment variables except `VAR1` for all roles. The `admin` role will inherit the global configuration and keep `VAR2` in the environment. So only `VAR1` and `VAR2` values will be kept in the environment for the `admin` role. + +### Environment options example 2 + +Here is an example global configuration: + +```json +{ + "options": { + "env": { + "default": "keep", + "delete": [ + "VAR1" + ] + } + } +} +``` + +The `delete` list is a list of environment variables that will be removed from the environment. + +```json +{ + "options": { + "env": { + "policy": "keep", + "delete": [ + "VAR1" + ] + } + }, + "roles": { + "admin": { + "options": { + "env": { + "policy": "inherit", + "delete": [ + "VAR2" + ] + } + } + } + } +} +``` + +This configuration will keep all environment variables except `VAR1` for all roles. The `admin` role will inherit the global configuration and remove `VAR2` from the environment. So only `VAR1` and `VAR2` values are removed from the environment for the `admin` role. + +### Environment options example 3 + +Here is an example global configuration: + +```json +{ + "options": { + "env": { + "default": "keep", + "check": [ + "VAR1" + ] + } + } +} +``` + +The `check` list is a list of environment variables that will be checked for unsafe characters. If an environment variable contains unsafe characters, it will be removed from the environment. diff --git a/book/src/guide/installation.md b/book/src/guide/installation.md index 32628469..3eefa16f 100644 --- a/book/src/guide/installation.md +++ b/book/src/guide/installation.md @@ -12,5 +12,29 @@ Install git 1. sudo ./configure.sh 1. sudo make install -> [!WARNING] -> **This installation process gives by default the entire privileges set for the user which execute sudo. This means that the user which install this program will be privileged.** \ No newline at end of file +
+ +The installation process requires CAP_SETFCAP privileges and also grants full privileges to the user who installs, making them privileged by default. + +
+ +### What does the installation script do? + +The installation script does the following: +- dependencies.sh + - Installs Rust and Cargo + - Copy cargo binary to /usr/local/bin directory + - Create a link /usr/local/bin/cargo to /bin/cargo + - Installs `pkgconf openssl curl cargo-make gcc llvm clang libcap libcap-ng libelf libxml2 linux-headers linux-api-headers make` + - Installs `bpf-linker` tool for `capable` eBPF tool +- configure.sh + - Deploy `sr` PAM module to /etc/pam.d directory + - Deploy `rootasrole.json` to /etc/security directory + - Set immutable attribute to `rootasrole.json` file. Note : It requires a compatible filesystem like ext2/3/4, xfs, btrfs, reisefs, etc. + - Define the user who installs the project in a role which has all capabilities for all commands. +- Executes make install + - Compiles `sr`, `chsr` and `capable` binaries + - Deploy `sr`, `chsr` and `capable` binaries to /usr/bin directory + - Set user and group ownership of `sr`, `chsr` and `capable` binaries to root + - Set file access permissions of `sr`, `chsr` and `capable` binaries to `r-xr-xr-x` + - Set file capabilities of `sr`, `chsr` and `capable` binaries \ No newline at end of file diff --git a/configure.sh b/configure.sh index 80c8e927..275cfd0b 100755 --- a/configure.sh +++ b/configure.sh @@ -18,6 +18,23 @@ if [ $(capsh --has-p=CAP_DAC_OVERRIDE; echo $?) != 0 ] || [ $(capsh --has-p=CAP_ exit 1 fi +export $(grep -h '^ID' /etc/*-release) + +echo "Configuration files installation" +echo "id : ${ID}" +if [ "${ID}" == "arch" ]; then + cp resources/arch_sr_pam.conf /etc/pam.d/sr || exit; +elif [ "${ID}" == "ubuntu" ] || [ "${ID}" == "debian" ]; then + cp resources/deb_sr_pam.conf /etc/pam.d/sr || exit; +elif [ "${ID}" == "centos" ] || [ "${ID}" == "fedora" ] || [[ "${ID}" == *"rhel"* ]]; then + cp resources/rh_sr_pam.conf /etc/pam.d/sr || exit; +else + echo "Unable to find a supported distribution, exiting..." + exit 3 +fi + + + if [ -e "/etc/security/rootasrole.json" ];then if [ $INSTALL_USER == "0" ]; then echo "Warning: You run this script as real root, so the administator role is defined for the root user" @@ -42,4 +59,6 @@ chmod 0644 /etc/pam.d/sr || exit chmod 0640 /etc/security/rootasrole.json || exit if [ $DOCKER -eq 0 ]; then chattr +i /etc/security/rootasrole.json || exit -fi \ No newline at end of file +fi + +echo "Configuration done, Ready to compile." diff --git a/dependencies.sh b/dependencies.sh index ee8ff9df..6d43d6f0 100755 --- a/dependencies.sh +++ b/dependencies.sh @@ -26,6 +26,7 @@ fi if [ ! -f "/usr/bin/cargo" ]; then cp ~/.cargo/bin/cargo /usr/bin + ln -s /usr/local/bin/cargo /bin/cargo echo "as $HOME/.cargo/bin/cargo cargo program is copied to /usr/bin" fi @@ -59,19 +60,6 @@ else exit 2 fi -echo "Install Rust Cargo compiler" -if [ "$(which cargo &>/dev/null ; echo $?)" -eq "0" ]; then - echo "Cargo is installed" -else - curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly ${YES} -fi - -if [ ! -f "/usr/bin/cargo" ]; then - mv -f ~/.cargo/bin/cargo /usr/local/bin - ln -s /usr/local/bin/cargo /bin/cargo - echo "$HOME/.cargo/bin/cargo program is copied to /usr/local/bin" -fi - # ask for user to install bpf-linker if [ "${YES}" == "-y" ]; then echo "cargo install bpf-linker into /usr/local/bin" @@ -90,21 +78,4 @@ else esac fi -export $(grep -h '^ID' /etc/*-release) - -echo "Configuration files installation" -echo "id : ${ID}" -if [ "${ID}" == "arch" ]; then - cp resources/arch_sr_pam.conf /etc/pam.d/sr || exit; - elif [ "${ID}" == "ubuntu" ] || [ "${ID}" == "debian" ]; then - cp resources/deb_sr_pam.conf /etc/pam.d/sr || exit; - elif [ "${ID}" == "centos" ] || [ "${ID}" == "fedora" ] || [[ "${ID}" == *"rhel"* ]]; then - cp resources/rh_sr_pam.conf /etc/pam.d/sr || exit; -else - echo "Unable to find a supported distribution, exiting..." - exit 3 -fi - - - -echo "configuration done. Ready to compile." +echo "dependencies installed. Ready to compile." diff --git a/resources/rootasrole.json b/resources/rootasrole.json index 67cc113d..958d3094 100644 --- a/resources/rootasrole.json +++ b/resources/rootasrole.json @@ -51,13 +51,13 @@ "TZ" ] }, - "allow-root": false, - "allow-bounding": false, + "root": "user", + "bounding": "strict", "wildcard-denied": ";&|" }, "roles": [ { - "name": "t_root", + "name": "r_root", "actors": [ { "type": "user", @@ -72,12 +72,31 @@ "setuid": "root", "setgid": "root", "capabilities": { - "default": "all" + "default": "all", + "sub": ["CAP_LINUX_IMMUTABLE"] } }, "commands": { "default": "all" } + }, + { + "name": "t_chsr", + "purpose": "Configure RootAsRole", + "cred": { + "setuid": "root", + "setgid": "root", + "capabilities": { + "default": "none", + "add": ["CAP_LINUX_IMMUTABLE"] + } + }, + "commands": { + "default": "none", + "add": [ + "/usr/bin/chsr .*" + ] + } } ] } diff --git a/src/api.rs b/src/api.rs index fea8298e..24178e01 100644 --- a/src/api.rs +++ b/src/api.rs @@ -134,15 +134,16 @@ impl PluginManager { ) -> PluginResultAction { debug!("Notifying role matchers"); let api = API.lock().unwrap(); + let mut result = PluginResultAction::Ignore; for plugin in api.role_matcher_plugins.iter() { debug!("Calling role matcher plugin"); match plugin(role, user, command, matcher) { PluginResultAction::Override => return PluginResultAction::Override, - PluginResultAction::Edit => continue, + PluginResultAction::Edit => result = PluginResultAction::Edit, PluginResultAction::Ignore => continue, } } - PluginResultAction::Ignore + result } pub fn notify_task_matcher( diff --git a/src/chsr/cli.pest b/src/chsr/cli.pest index e72829b1..f49f25d8 100644 --- a/src/chsr/cli.pest +++ b/src/chsr/cli.pest @@ -5,7 +5,7 @@ chsr = _{ name } list = { ("show" | "list" | "l") } set = { "set" | "s" } add = { "add" | "create" } -del = { "del" | "delete" | "unset" | "d" | "rm"} +del = { "delete" | "del" | "unset" | "d" | "rm"} purge = { "purge" } grant = { "grant" } revoke = { "revoke" } @@ -79,8 +79,14 @@ command_operations = _{ cmd_keyword ~ (cmd_setpolicy | cmd_checklisting) } cmd_setpolicy = { setpolicy ~ cmd_policy } cmd_policy = { "allow-all" | "deny-all" } -cmd_checklisting = { (whitelist | blacklist) ~ ((add | del) ~ cmd | purge) } -cmd = @{ name } +cmd_checklisting = { (whitelist | blacklist) ~ ((add | del) ~ quoted_cmd | purge) } +quoted_cmd = _{ ("'"|"\"")~ cmd ~ ("'"|"\"") | cmd } +cmd = ${ inner } +inner = @{ char* } +char = _{ + !("\"" | "'" | "\\") ~ ANY + | "\\" ~ ("'" | "\"" | "\\" | "/") +} // ======================== // credentials @@ -135,10 +141,10 @@ options_operations = { ("options" | "o") ~ opt_args } opt_args = _{ opt_show | opt_path | opt_env | opt_root | opt_bounding | opt_wildcard | opt_timeout } opt_show = _{ list ~ opt_show_arg? } -opt_show_arg = { "all" | "cmd" | "cred" | "env" | "root" | "bounding" | "wildcard-denied" | "timeout" } +opt_show_arg = { "all" | "cmd" | "cred" | "path" | "env" | "root" | "bounding" | "wildcard-denied" | "timeout" } opt_path = { "path" ~ (opt_path_args | help) } -opt_path_args = _{ opt_path_set | opt_path_setpolicy | opt_path_listing } +opt_path_args = _{ opt_path_setpolicy | opt_path_set | opt_path_listing } opt_path_set = _{ set ~ path } opt_path_setpolicy = _{ setpolicy ~ path_policy } path_policy = { "delete-all" | "keep-safe" | "keep-unsafe" | "inherit" } @@ -146,7 +152,7 @@ opt_path_listing = { (whitelist | blacklist) ~ (((add | del | set) ~ path) | path = @{ name } opt_env = { "env" ~ (opt_env_args | help) } -opt_env_args = _{ opt_env_set | opt_env_setpolicy | opt_env_listing } +opt_env_args = _{ opt_env_setpolicy | opt_env_set | opt_env_listing } opt_env_setpolicy = { setpolicy ~ env_policy } env_policy = { "delete-all" | "keep-all" | "inherit" } opt_env_listing = { (whitelist | blacklist | checklist) ~ (((add | del | set) ~ env_list) | purge) } @@ -190,6 +196,6 @@ opt_timeout_max_usage = { ASCII_DIGIT+ } assignment = _{ "=" | WHITESPACE } help = { "-h" | "--help" } -NOT_ESCAPE_QUOTE = @{ !"\\" ~ "\"" } +NOT_ESCAPE_QUOTE = @{ !"\\" ~ ("\""|"'") } WHITESPACE = _{ (!NOT_ESCAPE_QUOTE) ~ SEPARATOR+ } diff --git a/src/chsr/cli.rs b/src/chsr/cli.rs index b037a318..afcf9a38 100644 --- a/src/chsr/cli.rs +++ b/src/chsr/cli.rs @@ -24,10 +24,11 @@ use crate::{ SPathOptions, SPrivileged, STimeout, TimestampType, }, structs::{ - IdTask, SActor, SActorType, SCapabilities, SCommand, SGroups, SRole, STask, - SetBehavior, + IdTask, SActor, SActorType, SCapabilities, SCommand, SCommands, SGroups, SRole, + STask, SetBehavior, }, }, + util::escape_parser_string, }, rc_refcell, }; @@ -187,6 +188,7 @@ enum InputAction { Add, Del, Purge, + None, } #[derive(Debug, PartialEq, Eq)] @@ -228,7 +230,7 @@ struct Inputs { impl Default for Inputs { fn default() -> Self { Inputs { - action: InputAction::Help, + action: InputAction::None, setlist_type: None, timeout_type: None, timeout_duration: None, @@ -295,6 +297,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { } // === setpolicies === Rule::cmd_policy => { + inputs.action = InputAction::Set; if pair.as_str() == "deny-all" { inputs.cmd_policy = Some(SetBehavior::None); } else if pair.as_str() == "allow-all" { @@ -304,6 +307,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { } } Rule::caps_policy => { + inputs.action = InputAction::Set; if pair.as_str() == "deny-all" { inputs.cred_policy = Some(SetBehavior::None); } else if pair.as_str() == "allow-all" { @@ -313,6 +317,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { } } Rule::path_policy => { + inputs.action = InputAction::Set; if pair.as_str() == "delete-all" { inputs.options_path_policy = Some(PathBehavior::Delete); } else if pair.as_str() == "keep-safe" { @@ -326,6 +331,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { } } Rule::env_policy => { + inputs.action = InputAction::Set; if pair.as_str() == "delete-all" { inputs.options_env_policy = Some(EnvBehavior::Delete); } else if pair.as_str() == "keep-all" { @@ -337,7 +343,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { } } // === timeout === - Rule::opt_timeout_d_arg => { + Rule::time => { let mut reversed = pair.as_str().split(':').rev(); let mut duration: Duration = Duration::try_seconds(reversed.next().unwrap().parse::().unwrap_or(0)) @@ -359,7 +365,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { } inputs.timeout_duration = Some(duration); } - Rule::opt_timeout_t_arg => { + Rule::opt_timeout_type => { if pair.as_str() == "tty" { inputs.timeout_type = Some(TimestampType::TTY); } else if pair.as_str() == "ppid" { @@ -370,7 +376,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { warn!("Unknown timeout type: {}", pair.as_str()) } } - Rule::opt_timeout_m_arg => { + Rule::opt_timeout_max_usage => { inputs.timeout_max_usage = Some(pair.as_str().parse::().unwrap()); } // === roles === @@ -432,7 +438,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { } } // === commands === - Rule::cmd => { + Rule::inner => { inputs.cmd_id = Some(pair.as_str().to_string()); } // === credentials === @@ -510,6 +516,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { .insert_if_absent(pair.as_str().into()); } Rule::opt_root_args => { + inputs.action = InputAction::Set; if pair.as_str() == "privileged" { inputs.options_root = Some(SPrivileged::Privileged); } else if pair.as_str() == "user" { @@ -521,6 +528,7 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { } } Rule::opt_bounding_args => { + inputs.action = InputAction::Set; if pair.as_str() == "strict" { inputs.options_bounding = Some(SBounding::Strict); } else if pair.as_str() == "ignore" { @@ -534,6 +542,13 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) { Rule::wildcard_value => { inputs.options_wildcard = Some(pair.as_str().to_string()); } + Rule::all => { + if inputs.role_id.is_some() && !inputs.task_id.is_some() { + inputs.role_type = Some(RoleType::All); + } else if inputs.task_id.is_some() { + inputs.task_type = Some(TaskType::All); + } + } _ => { debug!("Unmatched rule: {:?}", pair.as_rule()); } @@ -558,7 +573,7 @@ fn rule_to_string(rule: &Rule) -> String { Rule::credentials_operations => "cred", Rule::cmd_checklisting => "whitelist, blacklist", Rule::cmd_policy => "allow-all or deny-all", - Rule::cmd => "a command line", + Rule::cmd | Rule::inner => "a command line", Rule::cred_c => "--caps \"cap_net_raw, cap_sys_admin, ...\"", Rule::cred_g => "--group \"g1,g2\"", Rule::cred_u => "--user \"u1\"", @@ -664,14 +679,18 @@ fn usage_concat(usages: &[&'static str]) -> String { usage } -pub fn main(storage: &Storage) -> Result> { +pub fn main(storage: &Storage, args: I) -> Result> +where + I: IntoIterator, + S: AsRef, +{ /*let binding = std::env::args().fold("\"".to_string(), |mut s, e| { s.push_str(&e); s.push_str("\" \""); s });*/ - let args = shell_words::join(std::env::args()); + let args = escape_parser_string(args); let args = Cli::parse(Rule::cli, &args); let args = match args { Ok(v) => v, @@ -746,12 +765,13 @@ pub fn main(storage: &Storage) -> Result> { for pair in args { recurse_pair(pair, &mut inputs); } - + debug!("Inputs : {:?}", inputs); match inputs { Inputs { action: InputAction::Help, .. } => { + debug!("chsr help"); println!("{}", LONG_ABOUT); println!("{}", RAR_USAGE_GENERAL); Ok(false) @@ -766,16 +786,27 @@ pub fn main(storage: &Storage) -> Result> { options_type, // in json .. } => match storage { - Storage::JSON(rconfig) => list_json( - rconfig, - role_id, - task_id, - options, - options_type, - task_type, - role_type, - ) - .and(Ok(false)), + Storage::JSON(rconfig) => { + debug!("chsr list"); + return match 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 @@ -785,21 +816,49 @@ pub fn main(storage: &Storage) -> Result> { setlist_type: None, options: false, actors: None, + role_type, .. } => match storage { Storage::JSON(rconfig) => { + debug!("chsr role r1 add|del"); let mut config = rconfig.as_ref().borrow_mut(); match action { InputAction::Add => { + //verify if role exists + if config.role(&role_id).is_some() { + return Err("Role already exists".into()); + } config .roles .push(rc_refcell!(SRole::new(role_id, Weak::new()))); Ok(true) } InputAction::Del => { + if config.role(&role_id).is_none() { + return Err("Role do not exists".into()); + } config.roles.retain(|r| r.as_ref().borrow().name != role_id); Ok(true) } + InputAction::Purge => { + if config.role(&role_id).is_none() { + return Err("Role do not exists".into()); + } + let role = config.role(&role_id).unwrap(); + match role_type { + Some(RoleType::Actors) => { + role.as_ref().borrow_mut().actors.clear(); + } + Some(RoleType::Tasks) => { + role.as_ref().borrow_mut().tasks.clear(); + } + None | Some(RoleType::All) => { + role.as_ref().borrow_mut().actors.clear(); + role.as_ref().borrow_mut().tasks.clear(); + } + } + Ok(true) + } _ => Ok(false), } } @@ -808,19 +867,34 @@ pub fn main(storage: &Storage) -> Result> { // chsr role r1 grant|revoke -u u1 -u u2 -g g1,g2 action, role_id: Some(role_id), - actors: Some(actors), + actors: Some(mut actors), options: false, .. } => match storage { Storage::JSON(rconfig) => { + debug!("chsr role r1 grant|revoke"); let config = rconfig.as_ref().borrow_mut(); let role = config.role(&role_id).ok_or("Role not found")?; match action { InputAction::Add => { + //verify if actor is already in role + //remove already existing actors + actors.retain(|a| { + if role.as_ref().borrow().actors.contains(a) { + println!("Actor {} already in role", a); + false + } else { + true + } + }); role.as_ref().borrow_mut().actors.extend(actors); Ok(true) } InputAction::Del => { + //if actor is not in role, warns + if !role.as_ref().borrow().actors.contains(&actors[0]) { + println!("Actor {} not in role", actors[0]); + } role.as_ref() .borrow_mut() .actors @@ -843,13 +917,27 @@ pub fn main(storage: &Storage) -> Result> { cred_caps: None, cred_setuid: None, cred_setgid: None, + task_type, + cmd_policy: None, + cred_policy: None, .. } => match storage { Storage::JSON(rconfig) => { + debug!("chsr role r1 task t1 add|del"); let config = rconfig.as_ref().borrow_mut(); let role = config.role(&role_id).ok_or("Role not found")?; match action { InputAction::Add => { + //verify if task exists + if role + .as_ref() + .borrow() + .tasks + .iter() + .any(|t| t.as_ref().borrow().name == task_id) + { + return Err("Task already exists".into()); + } role.as_ref() .borrow_mut() .tasks @@ -857,18 +945,52 @@ pub fn main(storage: &Storage) -> Result> { Ok(true) } InputAction::Del => { + if role + .as_ref() + .borrow() + .tasks + .iter() + .all(|t| t.as_ref().borrow().name != task_id) + { + return Err("Task do not exists".into()); + } role.as_ref() .borrow_mut() .tasks .retain(|t| t.as_ref().borrow().name != task_id); Ok(true) } + InputAction::Purge => { + let borrow = &role.as_ref().borrow(); + let task = borrow.task(&task_id).expect("Task do not exists".into()); + match task_type { + Some(TaskType::Commands) => { + task.as_ref().borrow_mut().commands.add.clear(); + task.as_ref().borrow_mut().commands.sub.clear(); + task.as_ref().borrow_mut().commands.default_behavior = None; + } + Some(TaskType::Credentials) => { + task.as_ref().borrow_mut().cred.capabilities = None; + task.as_ref().borrow_mut().cred.setuid = None; + task.as_ref().borrow_mut().cred.setgid = None; + } + None | Some(TaskType::All) => { + task.as_ref().borrow_mut().commands.add.clear(); + task.as_ref().borrow_mut().commands.sub.clear(); + task.as_ref().borrow_mut().commands.default_behavior = None; + task.as_ref().borrow_mut().cred.capabilities = None; + task.as_ref().borrow_mut().cred.setuid = None; + task.as_ref().borrow_mut().cred.setgid = None; + } + } + Ok(true) + } _ => Ok(false), } } }, Inputs { - //chsr role r1 task t1 cred --caps "cap_net_raw,cap_sys_admin" + //chsr role r1 task t1 cred set --caps "cap_net_raw,cap_sys_admin" action: InputAction::Set, role_id: Some(role_id), task_id: Some(task_id), @@ -876,9 +998,11 @@ pub fn main(storage: &Storage) -> Result> { cred_setuid, cred_setgid, cmd_id: None, + cmd_policy: None, .. } => match storage { Storage::JSON(rconfig) => { + debug!("chsr role r1 task t1 cred"); let config = rconfig.as_ref().borrow_mut(); match config.task(&role_id, &task_id) { Ok(task) => { @@ -898,15 +1022,57 @@ pub fn main(storage: &Storage) -> Result> { } } }, + Inputs { + //chsr role r1 task t1 cred unset --caps "cap_net_raw,cap_sys_admin" + action: InputAction::Del, + role_id: Some(role_id), + task_id: Some(task_id), + cred_caps, + cred_setuid, + cred_setgid, + cmd_id: None, + cmd_policy: None, + .. + } => match storage { + Storage::JSON(rconfig) => { + debug!("chsr role r1 task t1 cred unset"); + let config = rconfig.as_ref().borrow_mut(); + match config.task(&role_id, &task_id) { + Ok(task) => { + if let Some(caps) = cred_caps { + if caps.is_empty() { + task.as_ref().borrow_mut().cred.capabilities = None; + } else if let Some(ccaps) = + task.as_ref().borrow_mut().cred.capabilities.as_mut() + { + ccaps.add.drop_all(caps); + } else { + return Err("No capabilities to remove".into()); + } + } + if let Some(_) = cred_setuid { + task.as_ref().borrow_mut().cred.setuid = None; + } + if let Some(_) = cred_setgid { + task.as_ref().borrow_mut().cred.setgid = None; + } + Ok(true) + } + Err(e) => Err(e), + } + } + }, Inputs { action, role_id: Some(role_id), task_id: Some(task_id), setlist_type: Some(setlist_type), cred_caps: Some(cred_caps), + cmd_policy: None, .. } => match storage { Storage::JSON(rconfig) => { + debug!("chsr role r1 task t1 cred caps"); let config = rconfig.as_ref().borrow_mut(); let task = config.task(&role_id, &task_id)?; match setlist_type { @@ -982,6 +1148,7 @@ pub fn main(storage: &Storage) -> Result> { .. } => match storage { Storage::JSON(rconfig) => { + debug!("chsr role r1 task t1 cred setpolicy"); let config = rconfig.as_ref().borrow_mut(); let task = config.task(&role_id, &task_id)?; if task.as_ref().borrow_mut().cred.capabilities.is_none() { @@ -1001,6 +1168,26 @@ pub fn main(storage: &Storage) -> Result> { Ok(true) } }, + Inputs { + action: InputAction::Set, + role_id: Some(role_id), + task_id: Some(task_id), + cmd_policy: Some(cmd_policy), + .. + } => match storage { + Storage::JSON(rconfig) => { + debug!("chsr role r1 task t1 cmd setpolicy"); + let config = rconfig.as_ref().borrow_mut(); + let task = config.task(&role_id, &task_id)?; + + task.as_ref() + .borrow_mut() + .commands + .default_behavior + .replace(cmd_policy); + Ok(true) + } + }, Inputs { // chsr role r1 task t1 command whitelist add c1 action, @@ -1011,11 +1198,22 @@ pub fn main(storage: &Storage) -> Result> { .. } => match storage { Storage::JSON(rconfig) => { + debug!("chsr role r1 task t1 command whitelist add c1"); let config = rconfig.as_ref().borrow_mut(); let task = config.task(&role_id, &task_id)?; match setlist_type { SetListType::WhiteList => match action { InputAction::Add => { + //verify if command exists + if task + .as_ref() + .borrow() + .commands + .add + .contains(&SCommand::Simple(cmd_id.clone())) + { + return Err("Command already exists".into()); + } task.as_ref() .borrow_mut() .commands @@ -1023,11 +1221,25 @@ pub fn main(storage: &Storage) -> Result> { .push(SCommand::Simple(cmd_id)); } InputAction::Del => { - task.as_ref() - .borrow_mut() + //if command is not in task, warns + if !task + .as_ref() + .borrow() .commands .add - .retain(|c| c != &SCommand::Simple(cmd_id.clone())); + .contains(&SCommand::Simple(cmd_id.clone())) + { + println!("Command {} not in task", cmd_id); + } + task.as_ref().borrow_mut().commands.add.retain(|c| { + debug!( + "'{:?}' != '{:?}' : {}", + c, + &SCommand::Simple(cmd_id.clone()), + *c != SCommand::Simple(cmd_id.clone()) + ); + *c != SCommand::Simple(cmd_id.clone()) + }); } _ => { return Err("Unknown action".into()); @@ -1035,6 +1247,16 @@ pub fn main(storage: &Storage) -> Result> { }, SetListType::BlackList => match action { InputAction::Add => { + //verify if command exists + if task + .as_ref() + .borrow() + .commands + .sub + .contains(&SCommand::Simple(cmd_id.clone())) + { + return Err("Command already exists".into()); + } task.as_ref() .borrow_mut() .commands @@ -1042,6 +1264,16 @@ pub fn main(storage: &Storage) -> Result> { .push(SCommand::Simple(cmd_id)); } InputAction::Del => { + //if command is not in task, warns + if !task + .as_ref() + .borrow() + .commands + .sub + .contains(&SCommand::Simple(cmd_id.clone())) + { + println!("Command {} not in task", cmd_id); + } task.as_ref() .borrow_mut() .commands @@ -1066,6 +1298,7 @@ pub fn main(storage: &Storage) -> Result> { .. } => match storage { Storage::JSON(rconfig) => { + debug!("chsr role r1 task t1 command setpolicy"); let config = rconfig.as_ref().borrow_mut(); let task = config.task(&role_id, &task_id)?; task.as_ref() @@ -1106,6 +1339,7 @@ pub fn main(storage: &Storage) -> Result> { .. } => match storage { Storage::JSON(rconfig) => { + debug!("chsr o root set privileged"); perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { opt.as_ref().borrow_mut().root = Some(options_root); Ok(()) @@ -1131,7 +1365,7 @@ pub fn main(storage: &Storage) -> Result> { }, Inputs { // chsr o wildcard-denied set ";&*$" - action: InputAction::Set, + action, role_id, task_id, options_wildcard: Some(options_wildcard), @@ -1139,7 +1373,40 @@ pub fn main(storage: &Storage) -> Result> { } => match storage { Storage::JSON(rconfig) => { perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { - opt.as_ref().borrow_mut().wildcard_denied = Some(options_wildcard.clone()); + match action { + InputAction::Set => { + opt.as_ref().borrow_mut().wildcard_denied = + Some(options_wildcard.clone()); + } + InputAction::Add => { + let mut default_wildcard = opt + .as_ref() + .borrow() + .wildcard_denied + .clone() + .unwrap_or_default(); + default_wildcard.extend(options_wildcard.chars()); + opt.as_ref().borrow_mut().wildcard_denied = Some(default_wildcard); + } + InputAction::Del => { + if opt.as_ref().borrow().wildcard_denied.is_none() { + println!("No wildcard denied configured"); + return Ok(()); + } + opt.as_ref().borrow_mut().wildcard_denied.as_mut().map(|w| { + w.retain(|c| !options_wildcard.contains(c)); + }); + return Ok(()); + } + InputAction::Purge => { + opt.as_ref().borrow_mut().wildcard_denied = None; + return Ok(()); + } + _ => { + return Err("Unknown action".into()); + } + } + Ok(()) })?; Ok(true) @@ -1171,7 +1438,37 @@ pub fn main(storage: &Storage) -> Result> { return Err("Unknown setlist type".into()); } } - opt.as_ref().borrow_mut().path.as_mut().replace(path); + Ok(()) + })?; + Ok(true) + } + }, + Inputs { + // chsr o path whitelist set a:b:c + action: InputAction::Purge, + role_id, + task_id, + options_path: None, + options_type: Some(OptType::Path), + setlist_type, + .. + } => match storage { + Storage::JSON(rconfig) => { + perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { + let mut default_path = SPathOptions::default(); + let mut binding = opt.as_ref().borrow_mut(); + let path = binding.path.as_mut().unwrap_or(&mut default_path); + match setlist_type { + Some(SetListType::WhiteList) => { + path.add.clear(); + } + Some(SetListType::BlackList) => { + path.sub.clear(); + } + _ => { + return Err("Unknown setlist type".into()); + } + } Ok(()) })?; Ok(true) @@ -1206,7 +1503,6 @@ pub fn main(storage: &Storage) -> Result> { return Err("Internal Error: setlist type not found".into()); } } - opt.as_ref().borrow_mut().env.as_mut().replace(env); Ok(()) })?; Ok(true) @@ -1240,6 +1536,172 @@ pub fn main(storage: &Storage) -> Result> { Ok(true) } }, + Inputs { + // chsr o path whitelist add path1:path2:path3 + action, + role_id, + task_id, + options_path: Some(options_path), + options_type: Some(OptType::Path), + setlist_type, + .. + } => match storage { + Storage::JSON(rconfig) => { + perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { + let mut default_path = SPathOptions::default(); + let mut binding = opt.as_ref().borrow_mut(); + let path = binding.path.as_mut().unwrap_or(&mut default_path); + match setlist_type { + Some(SetListType::WhiteList) => match action { + InputAction::Add => { + path.add + .extend(options_path.split(':').map(|s| s.to_string())); + } + InputAction::Del => { + let hashset = options_path + .split(':') + .map(|s| s.to_string()) + .collect::>(); + path.add = path + .add + .difference(&hashset) + .cloned() + .collect::>(); + } + _ => { + return Err("Unknown action".into()); + } + }, + Some(SetListType::BlackList) => match action { + InputAction::Add => { + path.sub + .extend(options_path.split(':').map(|s| s.to_string())); + } + InputAction::Del => { + let hashset = options_path + .split(':') + .map(|s| s.to_string()) + .collect::>(); + path.sub = path + .sub + .difference(&hashset) + .cloned() + .collect::>(); + } + _ => { + return Err("Unknown action".into()); + } + }, + _ => { + return Err("Unknown setlist type".into()); + } + } + Ok(()) + })?; + Ok(true) + } + }, + Inputs { + // chsr o path whitelist add path1:path2:path3 + action, + role_id, + task_id, + options_env, + options_type: Some(OptType::Env), + setlist_type: Some(setlist_type), + .. + } => match storage { + Storage::JSON(rconfig) => { + perform_on_target_opt(rconfig, role_id, task_id, move |opt: Rc>| { + let mut default_env = SEnvOptions::default(); + let mut binding = opt.as_ref().borrow_mut(); + let env = binding.env.as_mut().unwrap_or(&mut default_env); + match setlist_type { + SetListType::WhiteList => match action { + InputAction::Add => { + if options_env.is_none() { + return Err("Empty list".into()); + } + env.keep.extend(options_env.as_ref().unwrap().clone()); + } + InputAction::Del => { + if options_env.is_none() { + return Err("Empty list".into()); + } + env.keep = env + .keep + .difference( + &options_env + .as_ref() + .unwrap() + .iter() + .cloned() + .collect::>(), + ) + .cloned() + .collect::>(); + } + InputAction::Purge => { + env.keep = LinkedHashSet::new(); + } + _ => { + return Err("Unknown action".into()); + } + }, + SetListType::BlackList => match action { + InputAction::Add => { + if options_env.is_none() { + return Err("Empty list".into()); + } + env.delete.extend(options_env.as_ref().unwrap().clone()); + } + InputAction::Del => { + if options_env.is_none() { + return Err("Empty list".into()); + } + env.delete = env + .delete + .difference(options_env.as_ref().unwrap()) + .cloned() + .collect::>(); + } + InputAction::Purge => { + env.delete = LinkedHashSet::new(); + } + _ => { + return Err("Unknown action".into()); + } + }, + SetListType::CheckList => match action { + InputAction::Add => { + if options_env.is_none() { + return Err("Empty list".into()); + } + env.check.extend(options_env.as_ref().unwrap().clone()); + } + InputAction::Del => { + if options_env.is_none() { + return Err("Empty list".into()); + } + env.check = env + .check + .difference(options_env.as_ref().unwrap()) + .cloned() + .collect::>(); + } + InputAction::Purge => { + env.check = LinkedHashSet::new(); + } + _ => { + return Err("Unknown action".into()); + } + }, + } + Ok(()) + })?; + Ok(true) + } + }, _ => Err("Unknown action".into()), } } @@ -1291,6 +1753,7 @@ 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) = config.role(&role_id) { list_task(task_id, role, options, options_type, task_type, role_type) diff --git a/src/chsr/main.rs b/src/chsr/main.rs index 0546a442..e379b14c 100644 --- a/src/chsr/main.rs +++ b/src/chsr/main.rs @@ -18,8 +18,7 @@ fn main() -> Result<(), Box> { subsribe("chsr"); drop_effective()?; register_plugins(); - read_effective(true).expect("Operation not permitted"); - let settings = config::get_settings().expect("Failed to get settings"); + let settings = config::get_settings().expect("Error on config read"); let config = match settings.clone().as_ref().borrow().storage.method { config::StorageMethod::JSON => Storage::JSON(read_json_config(settings.clone())?), _ => { @@ -29,7 +28,7 @@ fn main() -> Result<(), Box> { }; read_effective(false).expect("Operation not permitted"); - if cli::main(&config).is_ok_and(|b| b) { + if cli::main(&config, std::env::args()).is_ok_and(|b| b) { match config { Storage::JSON(config) => { debug!("Saving configuration"); @@ -41,3 +40,1768 @@ fn main() -> Result<(), Box> { Ok(()) } } + +#[cfg(test)] +mod tests { + use std::{io::Write, rc::Rc}; + + use self::common::{ + config::{RemoteStorageSettings, SettingsFile, ROOTASROLE}, + database::{options::*, structs::*, version::Versioning}, + }; + + use super::*; + use capctl::Cap; + use chrono::TimeDelta; + use common::config::Storage; + + fn setup() { + //Write json test json file + let mut file = std::fs::File::create(ROOTASROLE).unwrap(); + let mut settings = SettingsFile::default(); + settings.storage.method = config::StorageMethod::JSON; + settings.storage.settings = Some(RemoteStorageSettings::default()); + settings.storage.settings.as_mut().unwrap().path = Some(ROOTASROLE.into()); + settings.storage.settings.as_mut().unwrap().immutable = Some(false); + + let mut opt = Opt::default(); + + opt.timeout = Some(STimeout::default()); + opt.timeout.as_mut().unwrap().type_field = TimestampType::PPID; + opt.timeout.as_mut().unwrap().duration = TimeDelta::hours(15) + .checked_add(&TimeDelta::minutes(30)) + .unwrap() + .checked_add(&TimeDelta::seconds(30)) + .unwrap(); + opt.timeout.as_mut().unwrap().max_usage = Some(1); + + opt.path = Some(SPathOptions::default()); + opt.path.as_mut().unwrap().default_behavior = PathBehavior::Delete; + opt.path.as_mut().unwrap().add = vec!["path1".to_string(), "path2".to_string()] + .into_iter() + .collect(); + opt.path.as_mut().unwrap().sub = vec!["path3".to_string(), "path4".to_string()] + .into_iter() + .collect(); + + opt.env = Some(SEnvOptions::default()); + opt.env.as_mut().unwrap().default_behavior = EnvBehavior::Delete; + opt.env.as_mut().unwrap().keep = vec!["env1".into(), "env2".into()].into_iter().collect(); + opt.env.as_mut().unwrap().check = vec!["env3".into(), "env4".into()].into_iter().collect(); + opt.env.as_mut().unwrap().delete = vec!["env5".into(), "env6".into()].into_iter().collect(); + + opt.root = Some(SPrivileged::Privileged); + opt.bounding = Some(SBounding::Ignore); + opt.wildcard_denied = Some("*".to_string()); + + settings.config.as_ref().borrow_mut().options = Some(rc_refcell!(opt.clone())); + + settings.config.as_ref().borrow_mut().roles = vec![]; + + let mut role = SRole::default(); + role.name = "complete".to_string(); + role.actors = vec![ + SActor::from_user_id(0), + SActor::from_group_id(0), + SActor::from_group_vec_string(vec!["groupA", "groupB"]), + ]; + role.options = Some(rc_refcell!(opt.clone())); + let role = rc_refcell!(role); + + let mut task = STask::new(IdTask::Name("t_complete".to_string()), Rc::downgrade(&role)); + task.purpose = Some("complete".to_string()); + task.commands = SCommands::default(); + task.commands.default_behavior = Some(SetBehavior::All); + task.commands.add.push(SCommand::Simple("ls".to_string())); + task.commands.add.push(SCommand::Simple("echo".to_string())); + task.commands.sub.push(SCommand::Simple("cat".to_string())); + task.commands.sub.push(SCommand::Simple("grep".to_string())); + + task.cred = SCredentials::default(); + task.cred.setuid = Some(SActorType::Name("user1".to_string())); + task.cred.setgid = Some(SGroups::Multiple(vec![ + SActorType::Name("group1".to_string()), + SActorType::Name("group2".to_string()), + ])); + task.cred.capabilities = Some(SCapabilities::default()); + task.cred.capabilities.as_mut().unwrap().default_behavior = SetBehavior::All; + task.cred + .capabilities + .as_mut() + .unwrap() + .add + .add(Cap::LINUX_IMMUTABLE); + task.cred + .capabilities + .as_mut() + .unwrap() + .add + .add(Cap::NET_BIND_SERVICE); + task.cred + .capabilities + .as_mut() + .unwrap() + .sub + .add(Cap::SYS_ADMIN); + task.cred + .capabilities + .as_mut() + .unwrap() + .sub + .add(Cap::SYS_BOOT); + + task.options = Some(rc_refcell!(opt.clone())); + + role.as_ref().borrow_mut().tasks.push(rc_refcell!(task)); + settings.config.as_ref().borrow_mut().roles.push(role); + + let versionned = Versioning::new(settings.clone()); + + file.write_all( + serde_json::to_string_pretty(&versionned) + .unwrap() + .as_bytes(), + ) + .unwrap(); + + file.flush().unwrap(); + } + + fn teardown() { + //Remove json test file + std::fs::remove_file("target/rootasrole.json").unwrap(); + } + // we need to test every commands + // chsr r r1 create + // chsr r r1 delete + // chsr r r1 show (actors|tasks|all) + // chsr r r1 purge (actors|tasks|all) + // chsr r r1 grant -u user1 -g group1 group2&group3 + // chsr r r1 revoke -u user1 -g group1 group2&group3 + // chsr r r1 task t1 show (all|cmd|cred) + // chsr r r1 task t1 purge (all|cmd|cred) + // chsr r r1 t t1 add + // chsr r r1 t t1 del + // chsr r r1 t t1 commands show + // chsr r r1 t t1 cmd setpolicy (deny-all|allow-all) + // chsr r r1 t t1 cmd (whitelist|blacklist) (add|del) super command with spaces + // chsr r r1 t t1 credentials show + // chsr r r1 t t1 cred (unset|set) --caps capA,capB,capC --setuid user1 --setgid group1,group2 + // chsr r r1 t t1 cred caps setpolicy (deny-all|allow-all) + // chsr r r1 t t1 cred caps (whitelist|blacklist) (add|del) capA capB capC + // chsr (r r1) (t t1) options show (all|path|env|root|bounding|wildcard-denied) + // chsr o path set /usr/bin:/bin this regroups setpolicy delete and whitelist set + // chsr o path setpolicy (delete-all|keep-all|inherit) + // chsr o path (whitelist|blacklist) (add|del|set|purge) /usr/bin:/bin + + // chsr o env set MYVAR=1 VAR2=2 //this regroups setpolicy delete and whitelist set + // chsr o env setpolicy (delete-all|keep-all|inherit) + // chsr o env (whitelist|blacklist|checklist) (add|del|set|purge) MYVAR=1 + + // chsr o root (privileged|user|inherit) + // chsr o bounding (strict|ignore|inherit) + // chsr o wildcard-denied (set|add|del) * + + // chsr o timeout set --type tty --duration 5:00 --max_usage 1 + // chsr o t unset --type --duration --max_usage + + //TODO: verify values + #[test] + fn test_main_1() { + setup(); + + // lets test every commands + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "r1", "create"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "r1", "delete"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "show", "actors"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "show", "tasks"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "show", "all"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "purge", "actors"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "purge", "tasks"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "purge", "all"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "grant", + "-u", + "user1", + "-g", + "group1", + "-g", + "group2&group3" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "revoke", + "-u", + "user1", + "-g", + "group1", + "-g", + "group2&group3" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "task", "t_complete", "show", "all"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "task", "t_complete", "show", "cmd"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "task", + "t_complete", + "show", + "cred" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "task", + "t_complete", + "purge", + "all" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "task", + "t_complete", + "purge", + "cmd" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "task", + "t_complete", + "purge", + "cred" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "t", "t1", "add"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "t", "t1", "del"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cmd", + "setpolicy", + "deny-all" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cmd", + "setpolicy", + "allow-all" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cmd", + "whitelist", + "add", + "super command with spaces" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cmd", + "blacklist", + "add", + "super", + "command", + "with", + "spaces" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cmd", + "whitelist", + "del", + "super command with spaces" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cmd", + "blacklist", + "del", + "super command with spaces" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + // let settings = config::get_settings().expect("Failed to get settings"); + // assert!(cli::main( + // &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + // vec!["chsr", "r", "complete", "t", "t_complete", "credentials", "show"], + // ) + // .inspect_err(|e| { + // error!("{}", e); + // }) + // .inspect(|e| { + // debug!("{}",e); + // }) + // .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cred", + "unset", + "--caps", + "capA,capB,capC", + "--setuid", + "user1", + "--setgid", + "group1,group2" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cred", + "set", + "--caps", + "capA,capB,capC", + "--setuid", + "user1", + "--setgid", + "group1,group2" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cred", + "caps", + "setpolicy", + "deny-all" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cred", + "caps", + "setpolicy", + "allow-all" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cred", + "caps", + "whitelist", + "add", + "capA", + "capB", + "capC" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cred", + "caps", + "blacklist", + "add", + "capA", + "capB", + "capC" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cred", + "caps", + "whitelist", + "del", + "capA", + "capB", + "capC" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "cred", + "caps", + "blacklist", + "del", + "capA", + "capB", + "capC" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "options", "show", "all"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "options", "show", "path"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec!["chsr", "r", "complete", "options", "show", "bounding"], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "options", + "show", + "env" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "options", + "show", + "root" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "options", + "show", + "bounding" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "options", + "show", + "wildcard-denied" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| !b)); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "set", + "/usr/bin:/bin" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "setpolicy", + "delete-all" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "setpolicy", + "keep-unsafe" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "setpolicy", + "inherit" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "whitelist", + "add", + "/usr/bin:/bin" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "whitelist", + "del", + "/usr/bin:/bin" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "whitelist", + "set", + "/usr/bin:/bin" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "whitelist", + "purge" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "blacklist", + "add", + "/usr/bin:/bin" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "blacklist", + "del", + "/usr/bin:/bin" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "blacklist", + "set", + "/usr/bin:/bin" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "path", + "blacklist", + "purge" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "set", + "MYVAR,VAR2" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "setpolicy", + "delete-all" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "setpolicy", + "keep-all" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "setpolicy", + "inherit" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "whitelist", + "add", + "MYVAR" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "whitelist", + "del", + "MYVAR" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "whitelist", + "set", + "MYVAR" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "whitelist", + "purge" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "blacklist", + "add", + "MYVAR" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "blacklist", + "del", + "MYVAR" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "blacklist", + "set", + "MYVAR" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "blacklist", + "purge" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "checklist", + "add", + "MYVAR" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "checklist", + "del", + "MYVAR" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "checklist", + "set", + "MYVAR" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "env", + "checklist", + "purge" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "root", + "privileged" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "root", + "user" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "root", + "inherit" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "bounding", + "strict" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "bounding", + "ignore" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "bounding", + "inherit" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "wildcard-denied", + "set", + "*" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "wildcard-denied", + "add", + "*" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "wildcard-denied", + "del", + "*" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "timeout", + "set", + "--type", + "tty", + "--duration", + "5:00", + "--max-usage", + "1" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + let settings = config::get_settings().expect("Failed to get settings"); + assert!(cli::main( + &Storage::JSON(read_json_config(settings.clone()).expect("Failed to read json")), + vec![ + "chsr", + "r", + "complete", + "t", + "t_complete", + "o", + "t", + "unset", + "--type", + "--duration", + "--max-usage" + ], + ) + .inspect_err(|e| { + error!("{}", e); + }) + .inspect(|e| { + debug!("{}", e); + }) + .is_ok_and(|b| b)); + teardown(); + } +} diff --git a/src/config.rs b/src/config.rs index 30eedeb3..6ad4f341 100644 --- a/src/config.rs +++ b/src/config.rs @@ -47,15 +47,22 @@ // } // } +#[cfg(not(test))] pub const ROOTASROLE: &str = "/etc/security/rootasrole.json"; +#[cfg(test)] +pub const ROOTASROLE: &str = "target/rootasrole.json"; -use std::{cell::RefCell, error::Error, path::PathBuf, rc::Rc}; +use std::{cell::RefCell, error::Error, fs::File, path::PathBuf, rc::Rc}; +use ciborium::de; use serde::{Deserialize, Serialize}; use tracing::debug; use crate::{ - common::{dac_override_effective, immutable_effective, util::toggle_lock_config}, + common::{ + dac_override_effective, immutable_effective, read_effective, util::toggle_lock_config, + write_json_config, + }, rc_refcell, }; @@ -196,19 +203,9 @@ impl Default for RemoteStorageSettings { } } -fn write_json_config( - settings: &Versioning>>, -) -> Result<(), Box> { - let file = std::fs::File::create(ROOTASROLE)?; - serde_json::to_writer_pretty(file, &settings)?; - Ok(()) -} - pub fn save_settings(settings: Rc>) -> Result<(), Box> { debug!("Setting immutable privilege"); immutable_effective(true)?; - debug!("Setting dac privilege"); - dac_override_effective(true)?; let default_remote: RemoteStorageSettings = RemoteStorageSettings::default(); // remove immutable flag let into = ROOTASROLE.into(); @@ -225,7 +222,7 @@ pub fn save_settings(settings: Rc>) -> Result<(), Box>> = Versioning::new(settings.clone()); - write_json_config(&versionned)?; + write_json_config(&versionned, ROOTASROLE)?; debug!("Toggling immutable off for config file"); toggle_lock_config(path, false)?; debug!("Resetting dac privilege"); @@ -240,8 +237,21 @@ pub fn get_settings() -> Result>, Box> { if !std::path::Path::new(ROOTASROLE).exists() { return Ok(rc_refcell!(SettingsFile::default())); } - let file = std::fs::File::open(ROOTASROLE).expect("Failed to open file"); - let value: Versioning = serde_json::from_reader(file).unwrap_or_default(); + // if user does not have read permission, try to enable privilege + let file = std::fs::File::open(ROOTASROLE).or_else(|e| { + debug!( + "Error opening file without privilege, trying with privileges: {}", + e + ); + read_effective(true).or(dac_override_effective(true))?; + std::fs::File::open(ROOTASROLE) + })?; + let value: Versioning = serde_json::from_reader(file) + .inspect_err(|e| { + debug!("Error reading file: {}", e); + }) + .unwrap_or_default(); + read_effective(false).or(dac_override_effective(false))?; debug!("{}", serde_json::to_string_pretty(&value)?); let settingsfile = rc_refcell!(value.data); if Migration::migrate( diff --git a/src/database/finder.rs b/src/database/finder.rs index 2bff8e1c..1a77406a 100644 --- a/src/database/finder.rs +++ b/src/database/finder.rs @@ -819,8 +819,8 @@ fn plugin_role_match( } PluginResultAction::Edit => { debug!("Plugin edit"); - if !min_role.fully_matching() - || (matcher.fully_matching() && matcher.score < min_role.score) + if !min_role.command_matching() + || (matcher.command_matching() && matcher.score.cmd_min < min_role.score.cmd_min) { *min_role = matcher; *nmatch = 1; @@ -832,6 +832,7 @@ fn plugin_role_match( } PluginResultAction::Ignore => {} } + debug!("nmatch = {}", nmatch); } impl TaskMatcher for Rc> { diff --git a/src/database/mod.rs b/src/database/mod.rs index 7092468e..e2cb02e4 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,6 +1,7 @@ use std::{cell::RefCell, error::Error, rc::Rc}; use crate::common::config::save_settings; +use crate::common::read_effective; use crate::common::util::toggle_lock_config; use crate::common::version::PACKAGE_VERSION; @@ -13,6 +14,7 @@ use self::{migration::Migration, options::EnvKey, structs::SConfig, version::Ver use super::config::SettingsFile; use super::util::warn_if_mutable; +use super::write_json_config; use super::{ config::{RemoteStorageSettings, ROOTASROLE}, dac_override_effective, immutable_effective, @@ -102,41 +104,39 @@ pub fn save_json( } debug!("Setting immutable privilege"); immutable_effective(true)?; - debug!("Setting dac privilege"); - dac_override_effective(true)?; debug!("Toggling immutable on for config file"); toggle_lock_config(path, true)?; + immutable_effective(false)?; debug!("Writing config file"); let versionned: Versioning>> = Versioning { version: PACKAGE_VERSION.to_owned().parse()?, data: config, }; - write_json_config(&settings.as_ref().borrow(), versionned)?; + write_sconfig(&settings.as_ref().borrow(), versionned)?; debug!("Toggling immutable off for config file"); + immutable_effective(true)?; toggle_lock_config(path, false)?; - debug!("Resetting dac privilege"); - dac_override_effective(false)?; + debug!("Resetting immutable privilege"); immutable_effective(false)?; Ok(()) } -fn write_json_config( +fn write_sconfig( settings: &SettingsFile, config: Versioning>>, ) -> Result<(), Box> { let default_remote = RemoteStorageSettings::default(); - let file = std::fs::File::create( - settings - .storage - .settings - .as_ref() - .unwrap_or(&default_remote) - .path - .as_ref() - .unwrap_or(&ROOTASROLE.into()), - )?; - serde_json::to_writer_pretty(file, &config)?; + 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(()) } diff --git a/src/database/options.rs b/src/database/options.rs index c32a0186..f00aa853 100644 --- a/src/database/options.rs +++ b/src/database/options.rs @@ -2,6 +2,7 @@ use std::{borrow::Borrow, cell::RefCell, path::PathBuf, rc::Rc}; use chrono::Duration; +use ciborium::de; use libc::PATH_MAX; use linked_hash_set::LinkedHashSet; use pcre2::bytes::Regex; @@ -134,7 +135,7 @@ pub struct EnvKey { value: String, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct SEnvOptions { #[serde(rename = "default", default, skip_serializing_if = "is_default")] pub default_behavior: EnvBehavior, @@ -183,7 +184,7 @@ pub enum SPrivileged { Inherit, } -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct Opt { #[serde(skip_serializing_if = "Option::is_none")] @@ -253,6 +254,7 @@ impl Default for SPathOptions { impl EnvKey { pub fn new(s: String) -> Result { + debug!("Creating env key: {}", s); if Regex::new("^[a-zA-Z_]+[a-zA-Z0-9_]*$") // check if it is a valid env name .unwrap() .is_match(s.as_bytes()) diff --git a/src/database/structs.rs b/src/database/structs.rs index 0086a536..f06aae73 100644 --- a/src/database/structs.rs +++ b/src/database/structs.rs @@ -10,6 +10,7 @@ use serde::{ }; use serde_json::{Map, Value}; use strum::{Display, EnumIs}; +use tracing::debug; use std::{ cell::RefCell, diff --git a/src/database/version.rs b/src/database/version.rs index 1b34f912..9c0bc3da 100644 --- a/src/database/version.rs +++ b/src/database/version.rs @@ -34,35 +34,6 @@ impl Default for Versioning { } } -impl Versioning { - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - // Deserialize into the intermediate representation - let mut intermediate: Versioning = - ::deserialize(deserializer)?; - // Check version and perform migrations if necessary - if Migration::migrate( - &intermediate.version, - &mut intermediate.data, - JSON_MIGRATIONS, - ) - .and_then(|b| { - intermediate.version = version::PACKAGE_VERSION.to_owned().parse()?; - debug!("Migrated from {}", intermediate.version); - Ok(b) - }) - .is_err() - { - return Err(serde::de::Error::custom("Failed to migrate data")); - } - - // Return the migrated data - Ok(intermediate.data) - } -} - pub(crate) const JSON_MIGRATIONS: &[Migration] = &[]; pub(crate) const SETTINGS_MIGRATIONS: &[Migration] = &[]; diff --git a/src/descriptions.rs b/src/descriptions.rs index 001ab0f1..b10248e6 100644 --- a/src/descriptions.rs +++ b/src/descriptions.rs @@ -42,7 +42,7 @@ pub fn get_capability_description(cap : &Cap) -> &'static str { Cap::SYS_PACCT => r#"Use acct(2)."#, Cap::SYS_PTRACE => r#"• Trace arbitrary processes using ptrace(2); • apply get_robust_list(2) to arbitrary processes; • transfer data to or from the memory of arbitrary processes using process_vm_readv(2) and process_vm_writev(2); • inspect processes using kcmp(2)."#, Cap::SYS_RAWIO => r#"• Perform I/O port operations (iopl(2) and ioperm(2)); • access /proc/kcore; • employ the FIBMAP ioctl(2) operation; • open devices for accessing x86 model-specific registers (MSRs, see msr(4)); • update /proc/sys/vm/mmap_min_addr; • create memory mappings at addresses below the value specified by /proc/sys/vm/mmap_min_addr; • map files in /proc/bus/pci; • open /dev/mem and /dev/kmem; • perform various SCSI device commands; • perform certain operations on hpsa(4) and cciss(4) devices; • perform a range of device-specific operations on other devices."#, - Cap::SYS_RESOURCE => r#"• Use reserved space on ext2 filesystems; • make ioctl(2) calls controlling ext3 journaling; • override disk quota limits; • increase resource limits (see setrlimit(2)); • override RLIMIT_NPROC resource limit; • override maximum number of consoles on console allocation; • override maximum number of keymaps; • allow more than 64hz interrupts from the real-time clock; • raise msg_qbytes limit for a System V message queue above the limit in /proc/sys/kernel/msgmnb (see msgop(2) and msgctl(2)); • allow the RLIMIT_NOFILE resource limit on the number of \"in- flight\" file descriptors to be bypassed when passing file descriptors to another process via a UNIX domain socket (see unix(7)); • override the /proc/sys/fs/pipe-size-max limit when setting the capacity of a pipe using the F_SETPIPE_SZ fcntl(2) command; • use F_SETPIPE_SZ to increase the capacity of a pipe above the limit specified by /proc/sys/fs/pipe-max-size; • override /proc/sys/fs/mqueue/queues_max, /proc/sys/fs/mqueue/msg_max, and /proc/sys/fs/mqueue/msgsize_max limits when creating POSIX message queues (see mq_overview(7)); • employ the prctl(2) PR_SET_MM operation; • set /proc/pid/oom_score_adj to a value lower than the value last set by a process with CAP_SYS_RESOURCE."#, + Cap::SYS_RESOURCE => r#"• Use reserved space on ext2 filesystems; • make ioctl(2) calls controlling ext3 journaling; • override disk quota limits; • increase resource limits (see setrlimit(2)); • override RLIMIT_NPROC resource limit; • override maximum number of consoles on console allocation; • override maximum number of keymaps; • allow more than 64hz interrupts from the real-time clock; • raise msg_qbytes limit for a System V message queue above the limit in /proc/sys/kernel/msgmnb (see msgop(2) and msgctl(2)); • allow the RLIMIT_NOFILE resource limit on the number of \"in-flight\" file descriptors to be bypassed when passing file descriptors to another process via a UNIX domain socket (see unix(7)); • override the /proc/sys/fs/pipe-size-max limit when setting the capacity of a pipe using the F_SETPIPE_SZ fcntl(2) command; • use F_SETPIPE_SZ to increase the capacity of a pipe above the limit specified by /proc/sys/fs/pipe-max-size; • override /proc/sys/fs/mqueue/queues_max, /proc/sys/fs/mqueue/msg_max, and /proc/sys/fs/mqueue/msgsize_max limits when creating POSIX message queues (see mq_overview(7)); • employ the prctl(2) PR_SET_MM operation; • set /proc/pid/oom_score_adj to a value lower than the value last set by a process with CAP_SYS_RESOURCE."#, Cap::SYS_TIME => r#"Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock."#, Cap::SYS_TTY_CONFIG => r#"Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals."#, Cap::SYSLOG => r#"(since Linux 2.6.37) • Perform privileged syslog(2) operations. See syslog(2) for information on which operations require privilege. • View kernel addresses exposed via /proc and other interfaces when /proc/sys/kernel/kptr_restrict has the value 1. (See the discussion of the kptr_restrict in proc(5).)"#, diff --git a/src/mod.rs b/src/mod.rs index 2d454797..c197a0f8 100644 --- a/src/mod.rs +++ b/src/mod.rs @@ -1,8 +1,11 @@ use capctl::{prctl, Cap, CapState}; -use std::ffi::CString; -use tracing::Level; +use serde::Serialize; +use std::{error::Error, ffi::CString, path::PathBuf}; +use tracing::{debug, Level}; use tracing_subscriber::util::SubscriberInitExt; +use self::config::ROOTASROLE; + pub mod api; pub mod config; pub mod database; @@ -92,3 +95,19 @@ pub fn immutable_effective(enable: bool) -> Result<(), capctl::Error> { pub fn activates_no_new_privs() -> Result<(), capctl::Error> { prctl::set_no_new_privs() } + +pub fn write_json_config>( + settings: &T, + path: S, +) -> Result<(), Box> { + let file = std::fs::File::create(path).or_else(|e| { + debug!( + "Error creating file without privilege, trying with privileges: {}", + e + ); + read_effective(true).or(dac_override_effective(true))?; + std::fs::File::create(ROOTASROLE) + })?; + serde_json::to_writer_pretty(file, &settings)?; + Ok(()) +} diff --git a/src/plugin/hierarchy.rs b/src/plugin/hierarchy.rs index d297e7d3..63bdd1fb 100644 --- a/src/plugin/hierarchy.rs +++ b/src/plugin/hierarchy.rs @@ -8,6 +8,7 @@ use crate::common::{ }, }; +use ciborium::de; use serde::Deserialize; use tracing::{debug, warn}; @@ -40,11 +41,13 @@ fn find_in_parents( debug!("Checking parent role {}", parent); match role.as_ref().borrow().tasks.matches(user, command) { Ok(matches) => { + debug!("Parent role {} matched", parent); if !matcher.command_matching() || (matches.command_matching() && matches.score.cmd_cmp(&matcher.score) == Ordering::Less) { - matcher.score = matches.score; + debug!("Parent role {} is better", parent); + matcher.score.cmd_min = matches.score.cmd_min; matcher.settings = matches.settings; result = PluginResultAction::Edit; } diff --git a/src/util.rs b/src/util.rs index e8a1074e..bbcc08bc 100644 --- a/src/util.rs +++ b/src/util.rs @@ -72,20 +72,6 @@ pub fn warn_if_mutable(file: &File, return_err: bool) -> Result<(), Box String { - set.iter() - .fold(String::new(), |mut acc, cap| { - acc.push_str(&format!("CAP_{:?} ", cap)); - acc - }) - .trim_end() - .to_string() -} - -pub fn capset_to_vec(set: &capctl::CapSet) -> Vec { - set.iter().map(|cap| cap.to_string()).collect() -} - //parse string iterator to capset pub fn parse_capset_iter<'a, I>(iter: I) -> Result where @@ -123,6 +109,24 @@ pub fn capabilities_are_exploitable(caps: &CapSet) -> bool { || caps.has(Cap::MKNOD) } +pub fn escape_parser_string(s: I) -> String +where + I: IntoIterator, + S: AsRef, +{ + s.into_iter() + .map(|s| { + let s = s.as_ref(); + if s.contains(' ') { + format!("\"{}\"", s.replace("\"", "\\\"")) + } else { + s.to_string() + } + }) + .collect::>() + .join(" ") +} + #[cfg(test)] pub(super) mod test { pub fn test_resources_folder() -> std::path::PathBuf {