diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 18afe9c7..3657f2da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,6 @@ on: push: branches: - 'main' - - 'reorganisation' pull_request: branches: - 'main' diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 26608a1a..5e7ac21d 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -17,7 +17,6 @@ on: push: branches: - 'main' - - 'reorganisation' pull_request: branches: - 'main' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5feac47f..9215ccf2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,6 @@ on: push: branches: - 'main' - - 'reorganisation' pull_request: branches: - 'main' diff --git a/Cargo.toml b/Cargo.toml index 7cadf4df..9ebe0f0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,8 +109,13 @@ section = "admin" priority = "optional" assets = [ ["target/release/sr", "usr/bin/sr", "0555"], - ["target/release/chsr", "usr/bin/chsr", "0555"] + ["target/release/chsr", "usr/bin/chsr", "0555"], + ["target/man/sr.8.gz", "usr/share/man/man8/sr.8.gz", "0644"], + ["target/man/chsr.8.gz", "usr/share/man/man8/chsr.8.gz", "0644"], + ["target/man/fr/sr.8.gz", "usr/share/man/fr/man8/sr.8.gz", "0644"], + ["target/man/fr/chsr.8.gz", "usr/share/man/fr/man8/chsr.8.gz", "0644"] ] +preserve-symlinks = true conf-files = ["/etc/pam.d/sr", "/etc/security/rootasrole.json"] maintainer-scripts = "target/release/" extended-description = "RootAsRole is a project to allow Linux/Unix administrators to delegate their administrative tasks access rights to multiple co-administrators through RBAC model and Linux Capabilities features." @@ -121,6 +126,10 @@ assets = [ { source = "target/release/chsr", dest = "/usr/bin/chsr", user = "root", group = "root", mode = "0555" }, { source = "resources/rh/rh_sr_pam.conf", dest = "/etc/pam.d/sr", user = "root", group = "root", mode = "0644", config = true }, { source = "resources/rootasrole.json", dest = "/etc/security/rootasrole.json", user = "root", group = "root", mode = "0644", config = true }, + { source = "target/man/sr.8.gz", dest = "/usr/share/man/man8/sr.8.gz", user = "root", group = "root", mode = "0644", doc = true }, + { source = "target/man/chsr.8.gz", dest = "/usr/share/man/man8/chsr.8.gz" , user = "root", group = "root", mode = "0644", doc = true }, + { source = "target/man/fr/sr.8.gz", dest = "/usr/share/man/fr/man8/sr.8.gz", user = "root", group = "root", mode = "0644", doc = true }, + { source = "target/man/fr/chsr.8.gz", dest = "/usr/share/man/fr/man8/chsr.8.gz", user = "root", group = "root", mode = "0644", doc = true } ] post_install_script = "resources/rh/postinst.sh" post_install_script_prog = [ "/bin/sh", "-c" ] diff --git a/book/src/HISTORY.md b/book/src/HISTORY.md new file mode 100644 index 00000000..0f4f37a3 --- /dev/null +++ b/book/src/HISTORY.md @@ -0,0 +1,13 @@ +# HISTORY + +## 1.0.0 (August 2018) + +RootAsRole initiated by SIERA IRIT CNRS research team with Ahmad Samer WASAN as owner of the proof of concept. It is presented for the first time at "Le capitole du libre" in Toulouse. A paper is published%%cite{wazanRootAsRoleSecureAlternative2021}. + +## 2.0.0 (August 2019) + +RootAsRole is still a proof of concept but now proposes an eBPF subprogram to obtain the capabilities of a generic command. A paper is published at Computer and Security%%cite{wazanRootAsRoleSecurityModule2022}. + +## 3.0.0 (September 2024) + +RootAsRole direction is delegated to Eddie BILLOIR, a SIERA PhD student that contributed to the project all along its courses. It is now entirely rewritten in Rust, the eBPF subprogram is now in a separate project called RootAsRole-capable. The project takes a new direction and aims to be production ready with a massive reconception of every tools, a new configuration file format and complete documentation. \ No newline at end of file diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8fbe5f01..af24bd39 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -1,6 +1,7 @@ # Summary [Introduction](README.md) +[History](HISTORY.md) # User Guide diff --git a/book/src/guide/installation.md b/book/src/guide/installation.md index e28991ab..7f914906 100644 --- a/book/src/guide/installation.md +++ b/book/src/guide/installation.md @@ -8,9 +8,7 @@ Install git 1. git clone 1. cd RootAsRole - 1. . ./dependencies.sh - 1. sudo ./configure.sh - 1. make install + 1. cargo xtask install -bip sudo
@@ -20,21 +18,22 @@ The installation process requires CAP_SETFCAP privileges and also grants full pr ### 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 +The installation script parameters explaination: +- cargo xtask install -bip sudo + - (-b) Builds the project + - (-i) Installs necessary dependencies + - (-p) Use the `sudo` command to perform administrative tasks + +Install script does the following: +- Dependency Step : + - Installing necessary dependencies considering if compiling from source. +- Build Step : + - Building sr and chsr binaries +- Install Step : + - Copying sr and chsr binaries to /usr/bin + - Setting all capabilities on /usr/bin/sr + - Setting owners and permissions on /usr/bin/sr +- Configuration Step : + - Deploying /etc/pam.d/sr for PAM configuration + - Deploying /etc/security/rootasrole.json for configuration + - Setting immutable on /etc/security/rootasrole.json if filesytem supports it \ No newline at end of file diff --git a/resources/man/en_US.md b/resources/man/en_US.md new file mode 100644 index 00000000..66185f28 --- /dev/null +++ b/resources/man/en_US.md @@ -0,0 +1,94 @@ +% RootAsRole(8) System Manager's Manual +% Version 3.0.0 +% September 2024 + +# NAME +RootAsRole - An alternative to sudo/su commands that adheres to the principle of least privilege and provides more secure memory management. + +# SYNOPSIS +- **sr** [__OPTIONS__] [__COMMAND__]... +- **chsr** [__ARGUMENTS__] + + **chsr**'s arguments follow a grammar available at + +# DESCRIPTION +**RootAsRole** is a tool for administrators that provides a structured role-based access control (RBAC) system to delegate administrative tasks and access rights. It specifically supports __Linux capabilities(7)__ to minimize user privileges. + +The Role-Based Access Control (RBAC) model is based on sets of permissions assigned to users or groups. In RootAsRole, a role is a set of administrative tasks assigned to users. Tasks are commands with specific rights. Rights can include changing the user, changing the group, or/and using Linux capabilities. + +The **sr** command allows the execution of commands using a role. It requires a command to be executed as a mandatory parameter. It is also possible to specify a role and a task to select. + +There are cases where several tasks correspond to a user's command input. In such cases, sr will select the most precise and least privileged task. The notion of precision is based on how closely the RootAsRole policy matches the user's command. The more the user's profile matches the policy, the higher the level of precision. The same applies to the precision of the user's command compared to its specification in the policy. Similarly, the task with fewer privileges will be prioritized over a task with higher privileges, but only if the tasks are equally precise. Despite this intelligent selection, confusion can still arise, and an error message will be returned. + +Example of a confusion case: Two roles are assigned in the same way to a user, and among these roles, two tasks are entirely equivalent, but the configured environment variable are different for these two tasks. In this case, sr will display the error message "Permission denied" and log a warning that configuration must be fixed. This case should not happen if administrators are using **chsr**, the configuration tool. + +It is possible to change the user's prompt using the **-p** option. It is also possible to view the executor's rights using the **-i** option. The displayed information is very limited for the user. Otherwise, administrator can use **chsr** to obtain the complete policy. + +The **chsr** command is used to configure RootAsRole and its access control policy. It allows configuring roles, tasks, and permissions. The configuration is stored in the **/etc/security/rootasrole.json** file. If the file system supports it, the file is made immutable, requiring the CAP_LINUX_IMMUTABLE privilege to use **chsr**. The default RootAsRole policy grants to the installer the possibility to use **chsr** with the necessary privileges. + +The storage mode of the access control policy can be configured. By default, RootAsRole uses a JSON file. It is possible to change the storage mode by manually modifying the **/etc/security/rootasrole.json** file. + +Regarding authentication, RootAsRole uses Pluggable Authentication Module (PAM). The **/etc/pam.d/sr** file can be configured to change authentication behavior. + +The core of RootAsRole implements RBAC-0, a simplified version of RBAC. By default, it adds features in the form of plugins to implement certain RBAC-1 functionalities. RBAC-0 simply implements roles, tasks, and permissions. Plugins add role hierarchy and separation of duties. Plugins can only be implemented directly in the project. Another plugin allows testing the checksum of executed files. + +# OPTIONS + +**\-r, --role** <ROLE> + Choose a specific role. + + + +**\-t, --task** <TASK> + Choose a specific task within a role (requires --role) + + +**\-p, --prompt** <PROMPT> + Prompt to display when authenticating. + + +**\-i, --info** + Display rights of the executor. Information displayed is very limited. + + +**\-h, --help** + Print help (see more with '--help') + + +**\-v, --version** + Print version information + +# EXAMPLES + +**sr reboot** + Execute the reboot command (if the policy allows it). + +**sr -r dac chmod 644 /etc/foo/bar** + Execute the command chmod 644 /etc/foo/bar with the role dac (if the policy has a dac role and a task that allows the chmod command). + +# HISTORY + +You can find the history of RootAsRole in the website . + +# SECURITY RISKS + +RootAsRole is a security tool that can give a user full control of the system. An administrator can write an access control policy that gives too many privileges to a user. A Perl-compatible regular expression (pcre2) library is very complex and may accept unexpected special characters. + +It can be challenging to determine the necessary privileges for a command. For this, you can use the "capable" tool available at to determine the required capabilities for a command. However, this tool might give too many capabilities. It is recommended to verify if the capabilities are truly necessary, as in most cases, they are not. It is discouraged to use "capable" in production, as it is only for testing purposes. + +# SUPPORT + +For help, please visit or if you find a bug. + +# DISCLAIMER + +This program is provided "as is" without any warranty, to the extent permitted by law. The authors disclaim any responsibility for the quality or suitability of the program for a particular purpose. You use this program at your own risk. In case of problems, you are responsible for any necessary repairs or corrections. For more details, please refer to the GNU GPL version 3 or later . + +# AUTHOR +This manual was written by Eddie BILLOIR + +# LICENSE +GPLv3+: GNU GPL version 3 or later . + +# SEE ALSO +Linux capabilities(7), sudo(8), su(1) diff --git a/resources/man/fr_FR.md b/resources/man/fr_FR.md new file mode 100644 index 00000000..675044a0 --- /dev/null +++ b/resources/man/fr_FR.md @@ -0,0 +1,84 @@ +% RootAsRole(8) Manuel de l'administrateur système +% Version 3.0.0 +% Septembre 2024 + +# NAME +RootAsRole - Une alternative pour les commandes sudo/su respectant le principe du moindre privilège et une gestion de la mémoire plus sécurisée. + +# SYNOPSIS +- **sr** [__OPTIONS__] [__COMMAND__]... +- **chsr** [__ARGUMENTS__] + Les arguments suivent une grammaire disponible dans le code source à l'adresse + +# DESCRIPTION +**RootAsRole** est un outil pour les administrateurs qui fournit un système structuré de contrôle d'accès basé sur les rôles (RBAC) pour déléguer les tâches administratives et les droits d'accès. Il prend notamment en charge les __Linux capabilities(7)__ pour minimiser les privilèges des utilisateurs. + +Le modèle de Roles Based Access Control (RBAC) est basé sur des ensembles de permissions assignées à des utilisateurs ou des groupes. Pour RootAsRole, un rôle est un ensemble de tâches administratives assignées à des utilisateurs. Les tâches sont des commandes avec des droits à utiliser. Les droits peuvent être un changement d'utilisateur, un changement de groupe ou/et des Linux capabilities. + +La commande **sr** permet d'exécuter des commandes en utilisant un rôle. Il prends en paramètre obligatoire une commande à exécuter. Il est également possible de spécifier un rôle et une tâche à sélectionner. + +Il existe des cas où deux tâches correspondent à la commande d'un utilisateur. Dans ce cas, sr va sélectionner la tâche la plus précise et la moins privilégiée. La notion de précision est basée sur la précision de la politique RootAsRole comparée à l'occurence de la commande utilisateur. Plus le profil utilisateur et correspond à la politique, plus le niveau de précision est élevé. Il en est de même pour la précision de la commande de l'utilisateur vis-à-vis de sa spécification dans la politique. Pareillement, moins les droits sont élevés pour une tâche, plus la tâche sera prioritaire par rapport à une autre tâche. Le cas de la tâche moins privilégié n'est qu'uniquement si les tâches sont déjà avec le même niveau de précision. Malgré cette sélection intelligente, il reste des cas de confusion, ceux-ci renvoient un message d'erreur. + +Exemple d'un cas de confusion : Deux rôles sont assignés de la même manière à un utilisateur, parmi ces rôles, deux tâches sont totalement équivalentes mais les variables d'environment sont différents. Dans ce cas, sr affiche le message d'erreur "Permission denied" et fais un message warning dans les logs. + +Il est possible de changer le prompt de l'utilisateur en utilisant l'option **-p**. Il est également possible de voir les droits de l'exécuteur en utilisant l'option **-i**. Les informations affichées sont très limitées. + +La commande **chsr** sert à configurer RootAsRole et sa politique de contrôle d'accès. Elle permet de configurer les rôles, les tâches et les permissions. La configuration est stockée dans le fichier **/etc/security/rootasrole.json**. Si le système de fichier le permet, le fichier est rendu immuable, il faut alors le privilège CAP_LINUX_IMMUTABLE pour utiliser **chsr**. Pour cela, la politique par défaut de RootAsRole donne la permission à l'installateur d'utiliser **chsr** avec les privilèges nécessaires. + +Il est possible de configurer le mode de stockage de la politique de contrôle d'accès. Par défaut, RootAsRole utilise un fichier JSON. Il est possible de changer le mode de stockage en modifiant manuellement le fichier **/etc/security/rootasrole.json**. + +Concernant l'authentification, RootAsRole utilise PAM. Il est possible de configurer le fichier **/etc/pam.d/sr** pour changer le comportement de l'authentification. + +Le coeur de RootAsRole implémente RBAC-0, une version simplifiée de RBAC. Par défaut il ajoute des fonctionnalités sous forme de plugins pour implémenter certaines fonctionnalités de RBAC-1. RBAC-0 implémente simplement les rôles, les tâches et les permissions. Les plugins ajoutent la hiérarchie de rôles et séparation des devoirs. Les plugins sont uniquement implémentable directement dans le projet. Il y a également un autre plugin qui permet de tester la somme de contrôle des fichiers exécutés. + + + +# OPTIONS + +- **\-r, --role** + Role to select +- **\-t, --task** + Task to select (--role required) +- **\-p, --prompt** + Prompt to display +- **\-i, --info** + Display rights of executor +- **\-h, --help** + Print help (see more with '--help') +- **\-V, --version** + Print version information + +# EXAMPLES + +- **sr reboot** + Execute the command reboot. (If the policy is defined and allowed as well) + +- **sr -r dac chmod 644 /etc/foo/bar** + Execute the command chmod 644 /etc/foo/bar with the role dac (If the policy has a role dac and a task that allows chmod command) + +# HISTORIQUE + +Vous pouvez trouver l'historique de RootAsRole sur le site web . + +# RISQUES DE SÉCURITÉ + +RootAsRole est un outil de sécurité qui peut donner le contrôle complet du système à un utilisateur. Un administrateur peut écrire une politique de contrôle d'accès qui donne des droits trop élevés à un utilisateur. Une expression régulière perl (pcre2) est une librairie très complexe et peut accepter des caractères spéciaux inattendus. + +Il peut être difficile de déterminer les droits nécessaire pour une commande. Pour cela, il est possible d'utiliser l'outil "capable" disponible sur pour déterminer les capabilities nécessaires pour une commande. Cependant, il est également possible que cette commande donne trop de capabilities. Il est donc recommandé de vérifier si les capabilities sont bien nécessaires car dans la plupart des cas, les capabilities ne sont pas nécessaires. Il est fortement déconseillé d'utiliser cet outil en production. + +# SUPPORT + +Pour obtenir de l'aide, veuillez consulter ou si vous avez trouvé un bogue. + +# CLAUSE DE NON-RESPONSABILITÉ + +Ce programme est fourni « en l'état », sans aucune garantie, dans la limite permise par la loi. Les auteurs déclinent toute responsabilité quant à la qualité ou l'adéquation du programme à un usage particulier. Vous utilisez ce programme à vos propres risques. En cas de problème, vous êtes responsable des réparations ou corrections nécessaires. Pour plus de détails, veuillez consulter la licence GNU GPL version 3 ou ultérieure . + +# AUTEUR +Ce manuel a été écrit par Eddie BILLOIR + +# LICENCE +License GPLv3+: GNU GPL version 3 or later . + +# VOIR AUSSI +Linux capabilities(7), sudo(8), su(1) \ No newline at end of file diff --git a/src/sr/main.rs b/src/sr/main.rs index c088f40d..55785b9e 100644 --- a/src/sr/main.rs +++ b/src/sr/main.rs @@ -190,7 +190,12 @@ fn main() -> Result<(), Box> { subsribe("sr"); drop_effective()?; register_plugins(); - let args = getopt(std::env::args())?; + let args = std::env::args(); + if args.len() < 2 { + println!("{}", USAGE); + return Ok(()); + } + let args = getopt(args)?; if args.help { println!("{}", USAGE); diff --git a/xtask/src/configure.rs b/xtask/src/configure.rs index b83823d0..a9657ffa 100644 --- a/xtask/src/configure.rs +++ b/xtask/src/configure.rs @@ -34,15 +34,13 @@ fn is_running_in_container() -> bool { // Check cgroups for container-specific patterns if let Ok(file) = File::open("/proc/1/cgroup") { let reader = io::BufReader::new(file); - for line in reader.lines() { - if let Ok(line) = line { - if line.contains("docker") - || line.contains("kubepods") - || line.contains("lxc") - || line.contains("containerd") - { - return true; - } + for line in reader.lines().map_while(Result::ok) { + if line.contains("docker") + || line.contains("kubepods") + || line.contains("lxc") + || line.contains("containerd") + { + return true; } } } @@ -226,7 +224,7 @@ fn retrieve_real_user() -> Result, anyhow::Error> { if let Ok(sudo_user) = env::var("SUDO_USER") { let user = nix::unistd::User::from_name(&sudo_user).context("Failed to get the sudo user")?; - return Ok(user); + Ok(user) } else { let ruid = getresuid()?.real; let user = nix::unistd::User::from_uid(ruid).context("Failed to get the real user")?; @@ -257,9 +255,9 @@ pub fn configure(os: Option) -> Result<(), anyhow::Error> { os } else { OsTarget::detect() - .and_then(|t| { + .map(|t| { info!("Detected OS is : {}", t); - Ok(t) + t }) .context("Failed to detect the OS")? }; diff --git a/xtask/src/deploy/debian.rs b/xtask/src/deploy/debian.rs index 00306fd5..88574db8 100644 --- a/xtask/src/deploy/debian.rs +++ b/xtask/src/deploy/debian.rs @@ -3,14 +3,20 @@ use std::process::{Command, ExitStatus}; use anyhow::Context; use crate::{ - install::{self, dependencies::install_dependencies, InstallDependenciesOptions, Profile}, + installer::{self, dependencies::install_dependencies, InstallDependenciesOptions, Profile}, util::{detect_priv_bin, get_os, OsTarget}, }; use super::setup_maint_scripts; fn dependencies(os: &OsTarget, priv_bin: Option) -> Result { - install_dependencies(os, &["upx"], priv_bin).context("failed to install packaging dependencies") + install_dependencies(os, &["upx"], priv_bin) + .context("failed to install packaging dependencies")?; + Command::new("cargo") + .arg("install") + .arg("cargo-deb") + .status() + .context("failed to install cargo-deb") } pub fn make_deb( @@ -19,20 +25,20 @@ pub fn make_deb( priv_bin: &Option, ) -> Result<(), anyhow::Error> { let os = get_os(os)?; + let priv_bin = priv_bin.clone().or(detect_priv_bin()); + dependencies(&os, priv_bin.clone())?; - dependencies(&os, priv_bin.clone().or(detect_priv_bin()))?; - - install::dependencies(InstallDependenciesOptions { + installer::dependencies(InstallDependenciesOptions { os: Some(os), install_dependencies: true, dev: true, priv_bin: priv_bin.clone(), })?; - install::build(&install::BuildOptions { + installer::build(&installer::BuildOptions { profile, - toolchain: install::Toolchain::default(), + toolchain: installer::Toolchain::default(), clean_before: false, - privbin: Some("sudo".to_string()), + privbin: priv_bin, })?; setup_maint_scripts()?; diff --git a/xtask/src/deploy/mod.rs b/xtask/src/deploy/mod.rs index 63b77392..aabe797f 100644 --- a/xtask/src/deploy/mod.rs +++ b/xtask/src/deploy/mod.rs @@ -2,7 +2,7 @@ use std::{collections::HashSet, process::Command}; use clap::Parser; -use crate::{install::Profile, util::OsTarget}; +use crate::{installer::Profile, util::OsTarget}; mod debian; mod redhat; diff --git a/xtask/src/deploy/redhat.rs b/xtask/src/deploy/redhat.rs index 66569b0d..726a70c8 100644 --- a/xtask/src/deploy/redhat.rs +++ b/xtask/src/deploy/redhat.rs @@ -1,27 +1,38 @@ use std::process::Command; use crate::{ - install::{self, InstallDependenciesOptions, Profile}, - util::{get_os, OsTarget}, + installer::{self, InstallDependenciesOptions, Profile}, + util::{detect_priv_bin, get_os, OsTarget}, }; +fn install_dependencies() -> Result<(), anyhow::Error> { + Command::new("cargo") + .arg("install") + .arg("cargo-generate-rpm") + .status()?; + Ok(()) +} + pub fn make_rpm( os: Option, profile: Profile, exe: &Option, ) -> Result<(), anyhow::Error> { + install_dependencies()?; let os = get_os(os)?; - install::dependencies(InstallDependenciesOptions { + let exe: Option = exe.clone().or(detect_priv_bin()); + + installer::dependencies(InstallDependenciesOptions { os: Some(os), install_dependencies: true, dev: true, priv_bin: exe.clone(), })?; - install::build(&install::BuildOptions { + installer::build(&installer::BuildOptions { profile, - toolchain: install::Toolchain::default(), + toolchain: installer::Toolchain::default(), clean_before: false, - privbin: Some("sudo".to_string()), + privbin: exe.clone(), })?; Command::new("cargo").arg("generate-rpm").status()?; diff --git a/xtask/src/install/build.rs b/xtask/src/install/build.rs deleted file mode 100644 index 813ba219..00000000 --- a/xtask/src/install/build.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::process::Command; - -use tracing::debug; - -use super::BuildOptions; - -fn build_binary(name: &str, options: &BuildOptions, additionnal_args: Vec<&str>) { - let toolchain = format!("+{}", options.toolchain.to_string()); - let mut args = vec![&toolchain, "build", "--bin", name]; - if options.profile.is_release() { - args.push("--release"); - } - args.extend(additionnal_args); - debug!("Building {} binary with args: {:?}", name, args); - Command::new("cargo") - .args(args) - .status() - .expect(format!("failed to build {} binary", name).as_str()); -} - -pub fn build(options: &BuildOptions) -> Result<(), anyhow::Error> { - if options.clean_before { - Command::new("cargo") - .arg("clean") - .status() - .expect("failed to clean"); - } - build_binary("sr", options, vec![]); - build_binary("chsr", options, vec!["--no-default-features"]); - - Ok(()) -} diff --git a/xtask/src/install/util.rs b/xtask/src/install/util.rs deleted file mode 100644 index 0549f4ad..00000000 --- a/xtask/src/install/util.rs +++ /dev/null @@ -1,14 +0,0 @@ -use capctl::Cap; - -pub fn cap_clear(state: &mut capctl::CapState) -> Result<(), anyhow::Error> { - state.effective.clear(); - state.set_current()?; - Ok(()) -} - -pub fn cap_effective(state: &mut capctl::CapState, cap: Cap) -> Result<(), anyhow::Error> { - state.effective.clear(); - state.effective.add(cap); - state.set_current()?; - Ok(()) -} diff --git a/xtask/src/installer/build.rs b/xtask/src/installer/build.rs new file mode 100644 index 00000000..aa96d931 --- /dev/null +++ b/xtask/src/installer/build.rs @@ -0,0 +1,74 @@ +use std::{fs, os::unix, process::Command}; + +use anyhow::Context; +use tracing::debug; + +use super::BuildOptions; + +fn build_binary( + name: &str, + options: &BuildOptions, + additionnal_args: Vec<&str>, +) -> Result<(), anyhow::Error> { + let toolchain = format!("+{}", options.toolchain); + let mut args = vec![&toolchain, "build", "--bin", name]; + if options.profile.is_release() { + args.push("--release"); + } + args.extend(additionnal_args); + debug!("Building {} binary with args: {:?}", name, args); + Command::new("cargo").args(args).status()?; + Ok(()) +} + +pub fn build(options: &BuildOptions) -> Result<(), anyhow::Error> { + if options.clean_before { + Command::new("cargo") + .arg("clean") + .status() + .expect("failed to clean"); + } + build_binary("sr", options, vec![])?; + build_binary("chsr", options, vec!["--no-default-features"])?; + + build_manpages()?; + + Ok(()) +} + +fn build_manpages() -> Result<(), anyhow::Error> { + debug!("Building manpages"); + let _ = fs::remove_dir_all("target/man/"); + fs::create_dir_all("target/man/")?; + Command::new("pandoc") + .args([ + "-s", + "-t", + "man", + "resources/man/en_US.md", + "-o", + "target/man/sr.8", + ]) + .status()?; + fs::create_dir_all("target/man/fr")?; + Command::new("pandoc") + .args([ + "-s", + "-t", + "man", + "resources/man/fr_FR.md", + "-o", + "target/man/fr/sr.8", + ]) + .status()?; + debug!("Compressing manpages"); + Command::new("gzip") + .args(["target/man/sr.8", "target/man/fr/sr.8"]) + .status()?; + debug!("Making symlinks"); + unix::fs::symlink("sr.8.gz", "target/man/chsr.8.gz").context("Failed to create symlink")?; + unix::fs::symlink("sr.8.gz", "target/man/fr/chsr.8.gz").context("Failed to create symlink")?; + + debug!("Manpages built"); + Ok(()) +} diff --git a/xtask/src/install/dependencies.rs b/xtask/src/installer/dependencies.rs similarity index 88% rename from xtask/src/install/dependencies.rs rename to xtask/src/installer/dependencies.rs index 4919fa48..94d7cde2 100644 --- a/xtask/src/install/dependencies.rs +++ b/xtask/src/installer/dependencies.rs @@ -5,7 +5,7 @@ use capctl::CapState; use nix::unistd::geteuid; use tracing::info; -use crate::{install::OsTarget, util::get_os}; +use crate::{installer::OsTarget, util::get_os}; use super::InstallDependenciesOptions; @@ -43,10 +43,18 @@ fn required_dependencies(os: &OsTarget) -> &'static [&'static str] { fn development_dependencies(os: &OsTarget) -> &'static [&'static str] { match os { - OsTarget::Debian | OsTarget::Ubuntu => &["libpam0g-dev", "libpcre2-dev", "libclang-dev"], - OsTarget::RedHat => &["pcre2-devel", "clang-devel", "openssl-devel", "pam-devel"], - OsTarget::Fedora => &["clang-devel", "openssl-devel", "pam-devel"], - OsTarget::ArchLinux => &["clang", "pkg-config"], + OsTarget::Debian | OsTarget::Ubuntu => { + &["libpam0g-dev", "libpcre2-dev", "libclang-dev", "pandoc"] + } + OsTarget::RedHat => &[ + "pcre2-devel", + "clang-devel", + "openssl-devel", + "pam-devel", + "pandoc", + ], + OsTarget::Fedora => &["clang-devel", "openssl-devel", "pam-devel", "pandoc"], + OsTarget::ArchLinux => &["clang", "pkg-config", "pandoc"], } } diff --git a/xtask/src/install/install.rs b/xtask/src/installer/install.rs similarity index 68% rename from xtask/src/install/install.rs rename to xtask/src/installer/install.rs index e6538901..a722f084 100644 --- a/xtask/src/install/install.rs +++ b/xtask/src/installer/install.rs @@ -1,5 +1,6 @@ -use std::env::{self, current_exe}; +use std::env::{self, current_exe, set_current_dir}; use std::fs::{self, File}; +use std::io; use std::os::fd::AsRawFd; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -7,17 +8,18 @@ use std::str::FromStr; use capctl::{Cap, CapSet}; use nix::sys::stat::{fchmod, Mode}; use nix::unistd::{Gid, Uid}; +use nix::NixPath; use strum::EnumIs; use tracing::{debug, error, info}; -use crate::install::Profile; +use crate::installer::Profile; use crate::util::{detect_priv_bin, BOLD, RED, RST}; use anyhow::{anyhow, Context}; -use super::util::{cap_clear, cap_effective}; use super::{CHSR_DEST, SR_DEST}; +use crate::util::cap_clear; -fn copy_files(profile: &Profile) -> Result<(), anyhow::Error> { +fn copy_executables(profile: &Profile) -> Result<(), anyhow::Error> { let binding = std::env::current_dir()?; let cwd = binding .to_str() @@ -53,6 +55,68 @@ fn copy_files(profile: &Profile) -> Result<(), anyhow::Error> { Ok(()) } +fn copy_docs() -> Result<(), anyhow::Error> { + fn exit_directory() -> io::Result<()> { + set_current_dir("../..") + } + fn enter_directory() -> io::Result<()> { + set_current_dir("target/man/") + } + enter_directory()?; + + for file in glob::glob("**/*") + .map_err(|_| exit_directory()) + .expect("Failed to read glob pattern") + { + let file = file.inspect_err(|_| { + exit_directory().expect("Failed to exit directory"); + })?; + if file.is_dir() { + continue; + } + // file = "{code}/{file}" + //and copy the files to "/usr/share/man/{code}/man8/{file}" + + let file_name = &file + .file_name() + .ok_or_else(|| { + exit_directory().expect("Failed to exit directory"); + anyhow!("Failed to get the file name") + })? + .to_str() + .ok_or_else(|| { + exit_directory().expect("Failed to exit directory"); + anyhow!("Failed to get the file name") + })?; + let lang = file.parent(); + if lang.is_some_and(|p| !p.is_empty()) { + let lang = lang.unwrap(); + println!("lang: {:?}", lang); + let lang = lang.file_name().ok_or_else(|| { + exit_directory().expect("Failed to exit directory"); + anyhow!("Failed to get the language") + })?; + let lang = lang.to_str().ok_or_else(|| { + exit_directory().expect("Failed to exit directory"); + anyhow!("Failed to get the language") + })?; + let dest = format!("/usr/share/man/{}/man8/{}", lang, file_name); + debug!("Copying file: {:?} to {}", &file, dest); + fs::copy(&file, &dest).inspect_err(|_| { + exit_directory().expect("Failed to exit directory"); + })?; + } else { + let dest = format!("/usr/share/man/man8/{}", file_name); + debug!("Copying file: {:?} to {}", &file, dest); + fs::copy(&file, &dest).inspect_err(|_| { + exit_directory().expect("Failed to exit directory"); + })?; + } + } + exit_directory()?; + Ok(()) +} + fn chmod() -> Result<(), anyhow::Error> { let sr_file = File::open(SR_DEST)?; let chsr_file = File::open(CHSR_DEST)?; @@ -86,6 +150,13 @@ pub enum Elevated { No, } +fn cap_effective(state: &mut capctl::CapState, cap: Cap) -> Result<(), anyhow::Error> { + state.effective.clear(); + state.effective.add(cap); + state.set_current()?; + Ok(()) +} + pub fn install( priv_exe: &Option, profile: Profile, @@ -120,13 +191,13 @@ pub fn install( .or(priv_bin.as_ref()) .context("Privileged binary is required") .map_err(|_| { - return anyhow::Error::msg(format!( + anyhow::Error::msg(format!( "Please run {} as an administrator.", current_exe() .unwrap_or(PathBuf::from_str("the command").unwrap()) .to_str() .unwrap() - )); + )) })?; env::set_var("ROOTASROLE_INSTALLER_NESTED", "1"); tracing::warn!("Elevating privileges..."); @@ -141,13 +212,13 @@ pub fn install( .context("Failed to run privileged binary") .map_err(|e| { error!("{}", e); - return anyhow::Error::msg(format!( + anyhow::Error::msg(format!( "Failed to run privileged binary. Please run {} as an administrator.", current_exe() .unwrap_or(PathBuf::from_str("the command").unwrap()) .to_str() .unwrap() - )); + )) })?; return Ok(Elevated::Yes); } @@ -157,7 +228,9 @@ pub fn install( cap_effective(&mut state, Cap::DAC_OVERRIDE).context("Failed to raise DAC_OVERRIDE")?; // cp target/{release}/sr,chsr,capable /usr/bin - copy_files(&profile).context("Failed to copy sr and chsr files")?; + copy_executables(&profile).context("Failed to copy sr and chsr files")?; + + copy_docs().context("Failed to copy documentation files")?; // drop dac_override cap_clear(&mut state).context("Failed to drop effective DAC_OVERRIDE")?; @@ -185,7 +258,7 @@ pub fn install( if clean_after { std::process::Command::new("cargo") - .args(&["clean"]) + .args(["clean"]) .status() .context("Failed to clean the project")?; } diff --git a/xtask/src/install/mod.rs b/xtask/src/installer/mod.rs similarity index 92% rename from xtask/src/install/mod.rs rename to xtask/src/installer/mod.rs index aaca51a1..68ed7f32 100644 --- a/xtask/src/install/mod.rs +++ b/xtask/src/installer/mod.rs @@ -2,10 +2,9 @@ mod build; pub(crate) mod dependencies; pub(crate) mod install; mod uninstall; -mod util; -use std::collections::VecDeque; use std::str::FromStr; +use std::{collections::VecDeque, fmt::Display}; use chrono::{Datelike, NaiveDate, Utc}; use clap::{Parser, ValueEnum}; @@ -112,8 +111,8 @@ pub struct BuildOptions { pub clean_before: bool, } -impl ToString for Toolchain { - fn to_string(&self) -> String { +impl Display for Toolchain { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut s = self.channel.to_string(); if let Some(ref date) = self.date { s.push_str(&format!( @@ -126,7 +125,7 @@ impl ToString for Toolchain { if let Some(ref host) = self.host { s.push_str(&format!("-{}", host)); } - s + write!(f, "{}", s) } } @@ -155,13 +154,13 @@ pub enum Channel { Version(Version), } -impl ToString for Channel { - fn to_string(&self) -> String { +impl Display for Channel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Channel::Stable => "stable".to_string(), - Channel::Beta => "beta".to_string(), - Channel::Nightly => "nightly".to_string(), - Channel::Version(version) => version.to_string(), + Channel::Stable => write!(f, "stable"), + Channel::Beta => write!(f, "beta"), + Channel::Nightly => write!(f, "nightly"), + Channel::Version(v) => write!(f, "{}", v), } } } @@ -196,7 +195,7 @@ impl FromStr for Toolchain { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let mut parts: VecDeque<&str> = s.split('-').collect(); - if parts.len() < 1 { + if parts.is_empty() { return Ok(Toolchain::default()); } let channel = parts @@ -218,11 +217,11 @@ impl FromStr for Toolchain { let host = parts .iter() .fold(String::new(), |acc, x| format!("{}-{}", acc, x)); - return Ok(Toolchain { + Ok(Toolchain { channel, date, host: if host.is_empty() { None } else { Some(host) }, - }); + }) } } diff --git a/xtask/src/install/uninstall.rs b/xtask/src/installer/uninstall.rs similarity index 100% rename from xtask/src/install/uninstall.rs rename to xtask/src/installer/uninstall.rs diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 60a4aec0..b3c54cf1 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,6 +1,6 @@ mod configure; mod deploy; -mod install; +mod installer; pub mod util; use std::process::exit; @@ -19,18 +19,18 @@ pub struct Options { #[derive(Debug, Parser)] enum Command { #[cfg(feature = "cli")] - Dependencies(install::InstallDependenciesOptions), + Dependencies(installer::InstallDependenciesOptions), #[cfg(feature = "cli")] - Build(install::BuildOptions), + Build(installer::BuildOptions), #[cfg(feature = "cli")] - Install(install::InstallOptions), + Install(installer::InstallOptions), Configure { /// The OS target #[clap(long)] os: Option, }, - Uninstall(install::UninstallOptions), + Uninstall(installer::UninstallOptions), #[cfg(feature = "deploy")] Deploy(deploy::MakeOptions), } @@ -50,11 +50,11 @@ fn main() { let opts = Options::parse(); use Command::*; let ret = match opts.command { - Dependencies(opts) => install::dependencies(opts), - Build(opts) => install::build(&opts), - Install(opts) => install::install(&opts), - Configure { os } => install::configure(os), - Uninstall(opts) => install::uninstall(&opts), + Dependencies(opts) => installer::dependencies(opts), + Build(opts) => installer::build(&opts), + Install(opts) => installer::install(&opts), + Configure { os } => installer::configure(os), + Uninstall(opts) => installer::uninstall(&opts), Deploy(opts) => deploy::deploy(&opts), }; diff --git a/xtask/src/postinst.rs b/xtask/src/postinst.rs index d0d12443..2a5f308e 100644 --- a/xtask/src/postinst.rs +++ b/xtask/src/postinst.rs @@ -5,15 +5,16 @@ use tracing::warn; use util::{OsTarget, SettingsFile, ROOTASROLE}; mod configure; -mod install; +mod installer; mod util; fn main() { let action = args().nth(1); - match action { - Some(action) => match action.as_str() { + if let Some(action) = action { + match action.as_str() { "configure" => { - let res = install::install::install(&None, install::Profile::Release, false, false); + let res = + installer::install::install(&None, installer::Profile::Release, false, false); if let Err(e) = res { warn!("{:#}", e); std::process::exit(1); @@ -44,7 +45,6 @@ fn main() { } } _ => {} - }, - None => {} + } } } diff --git a/xtask/src/util.rs b/xtask/src/util.rs index 6ab67340..9de1f866 100644 --- a/xtask/src/util.rs +++ b/xtask/src/util.rs @@ -108,14 +108,15 @@ fn immutable_required_privileges(file: &File, effective: bool) -> Result<(), cap } fn read_or_dac_override(effective: bool) -> Result<(), capctl::Error> { - Ok(match effective { + match effective { false => { read_effective(false).and(dac_override_effective(false))?; } true => { read_effective(true).or(dac_override_effective(true))?; } - }) + } + Ok(()) } /// Set or unset the immutable flag on a file @@ -187,9 +188,9 @@ pub fn get_os(os: Option) -> Result { os } else { OsTarget::detect() - .and_then(|t| { + .map(|t| { debug!("Detected OS is : {}", t); - Ok(t) + t }) .context("Failed to detect the OS")? }) @@ -198,16 +199,22 @@ pub fn get_os(os: Option) -> Result { pub fn detect_priv_bin() -> Option { // is /usr/bin/sr exist ? if std::fs::metadata("/usr/bin/sr").is_ok() { - return Some("/usr/bin/sr".to_string()); + Some("/usr/bin/sr".to_string()) } else if std::fs::metadata("/usr/bin/sudo").is_ok() { - return Some("/usr/bin/sudo".to_string()); + Some("/usr/bin/sudo".to_string()) } else if std::fs::metadata("/usr/bin/doas").is_ok() { - return Some("/usr/bin/doas".to_string()); + Some("/usr/bin/doas".to_string()) } else { - return None; + None } } +pub fn cap_clear(state: &mut capctl::CapState) -> Result<(), anyhow::Error> { + state.effective.clear(); + state.set_current()?; + Ok(()) +} + #[cfg(test)] mod test { use std::{fs, path::PathBuf};