diff --git a/.cargo/config.toml b/.cargo/config.toml index 4e087b56..aa6c4905 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -42,7 +42,7 @@ coverage = [ [env] RAR_CFG_TYPE = "json" RAR_CFG_PATH = "/etc/security/rootasrole.json" -RAR_CFG_DATA_PATH = "/etc/security/rootasrole.json" +RAR_CFG_DATA_PATH = "/etc/security/rootasrole.d/" RAR_PAM_SERVICE = "dosr" RAR_BIN_PATH = "/usr/bin" RAR_CFG_IMMUTABLE = "true" @@ -66,4 +66,8 @@ RAR_BOUNDING = "strict" RAR_UMASK = "0022" RAR_MAX_LOCKFILE_RETRIES = "10" RAR_LOCKFILE_RETRY_INTERVAL = "1" -RAR_TIMEOUT_STORAGE = "/var/run/rar/ts" \ No newline at end of file +RAR_TIMEOUT_STORAGE = "/var/run/rar/ts" +RAR_WORKDIR_BEHAVIOR = "all" +RAR_WORKDIR_ADD_LIST = "" +RAR_WORKDIR_REMOVE_LIST = "" +#RAR_WORKDIR_FALLBACK = "" diff --git a/.github/workflows/install-tests.yml b/.github/workflows/install-tests.yml new file mode 100644 index 00000000..3c61e1c3 --- /dev/null +++ b/.github/workflows/install-tests.yml @@ -0,0 +1,167 @@ +name: Xtask Tests + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + install-debian: + runs-on: ubuntu-latest + container: + image: debian:stable + options: --privileged + steps: + - name: Install git and build dependencies + run: | + apt update -y + apt install -y git curl build-essential pkg-config libpam0g-dev + apt install -y sudo ca-certificates + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Configure PAM + run: | + mkdir -p /etc/pam.d + cat <<'EOF' > /etc/pam.d/dosr + #%PAM-1.0 + auth [success=1 default=ignore] pam_permit.so + auth requisite pam_permit.so + auth required pam_permit.so + account [success=1 default=ignore] pam_permit.so + account requisite pam_permit.so + account required pam_permit.so + session [success=1 default=ignore] pam_permit.so + session requisite pam_permit.so + session required pam_permit.so + EOF + + - name: Install RootAsRole + run: | + cargo xtask install --debug -bip sudo + + - name: Verify installation + run: | + command -v dosr + command -v chsr + dosr --version + dosr --help + dosr /usr/bin/chsr --help + dosr cat /etc/hostname + + install-rocky: + runs-on: ubuntu-latest + container: + image: rockylinux:9 + options: --privileged + steps: + - name: Install git and build dependencies + run: | + dnf update -y + dnf install -y 'dnf-command(config-manager)' || true + dnf config-manager --set-enabled crb || true + dnf install -y epel-release || true + dnf makecache -y + dnf install -y pandoc || true + dnf install -y git gcc pkg-config pam-devel + dnf install -y sudo ca-certificates + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Configure PAM + run: | + mkdir -p /etc/pam.d + cat <<'EOF' > /etc/pam.d/dosr + #%PAM-1.0 + auth [success=1 default=ignore] pam_permit.so + auth requisite pam_permit.so + auth required pam_permit.so + account [success=1 default=ignore] pam_permit.so + account requisite pam_permit.so + account required pam_permit.so + session [success=1 default=ignore] pam_permit.so + session requisite pam_permit.so + session required pam_permit.so + EOF + + - name: Install RootAsRole + run: | + cargo xtask install -bip sudo + + - name: Verify installation + run: | + command -v dosr + command -v chsr + dosr --version + dosr --help + dosr /usr/bin/chsr --help + + - name: Test basic functionality + run: | + dosr cat /etc/hostname + + install-opensuse: + runs-on: ubuntu-latest + container: + image: opensuse/tumbleweed:latest + options: --privileged + env: + HOME: /root + CARGO_HOME: /root/.cargo + RUSTUP_HOME: /root/.rustup + steps: + - name: Install git and build dependencies + run: | + zypper refresh + zypper install -y bash rustup git curl gcc pkg-config pam-devel + zypper install -y sudo + rustup toolchain install stable + rustup update + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure PAM + run: | + mkdir -p /etc/pam.d + cat <<'EOF' > /etc/pam.d/dosr + #%PAM-1.0 + auth [success=1 default=ignore] pam_permit.so + auth requisite pam_permit.so + auth required pam_permit.so + account [success=1 default=ignore] pam_permit.so + account requisite pam_permit.so + account required pam_permit.so + session [success=1 default=ignore] pam_permit.so + session requisite pam_permit.so + session required pam_permit.so + EOF + + - name: Install RootAsRole + run: | + cargo xtask install -bip sudo + + - name: Verify installation + run: | + command -v dosr + command -v chsr + dosr --version + dosr --help + dosr /usr/bin/chsr --help + + - name: Test basic functionality + run: | + dosr cat /etc/hostname \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 02d7e381..19be390c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,34 +1,8 @@ { - "files.associations": { - "capability.h": "c", - "random": "c", - "role_manager.h": "c", - "strings.h": "c", - "string.h": "c", - "stat.h": "c", - "limits.h": "c", - "xml_options.h": "c", - "rar_env.h": "c", - "errno.h": "c", - "stdlib.h": "c", - "syslog.h": "c", - "types.h": "c", - "array": "c", - "string_view": "c", - "initializer_list": "c", - "xml_manager.h": "c", - "cstring": "c", - "string": "c", - "sstream": "c", - "command.h": "c", - "unistd.h": "c", - "stdint.h": "c", - "criterion.h": "c", - "typeinfo": "c" - }, "rust-analyzer.linkedProjects": [ //"sudoers-reader/Cargo.toml", "Cargo.toml", + "rar-common/Cargo.toml", //"./capable/Cargo.toml" ], "c-cpp-flylint.clang.includePaths": [ diff --git a/Cargo.lock b/Cargo.lock index 183f17ac..f8daa6ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,9 +90,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -186,9 +186,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.58" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -223,9 +223,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -269,11 +269,12 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst 0.2.20", ] [[package]] @@ -520,9 +521,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -579,9 +580,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", @@ -601,9 +602,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", @@ -614,9 +615,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -635,40 +636,47 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.92" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "konst" -version = "0.3.16" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" dependencies = [ - "const_panic", - "konst_kernel", - "konst_proc_macros", - "typewit", + "konst_macro_rules", ] [[package]] -name = "konst_kernel" -version = "0.3.15" +name = "konst" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" dependencies = [ + "const_panic", + "konst_proc_macros", "typewit", ] +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "konst_proc_macros" -version = "0.3.10" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" +checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "landlock" @@ -683,9 +691,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libpam-sys" @@ -718,11 +726,11 @@ dependencies = [ [[package]] name = "libseccomp" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21c57fd8981a80019807b7b68118618d29a87177c63d704fc96e6ecd003ae5b3" +checksum = "0e5310a2c5b6ffbc094b5f70a2ca7b79ed36ad90e6f90994b166489a1bce3fcc" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", "libseccomp-sys", "pkg-config", @@ -730,9 +738,9 @@ dependencies = [ [[package]] name = "libseccomp-sys" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7cbbd4ad467251987c6e5b47d53b11a5a05add08f2447a9e2d70aef1e0d138" +checksum = "60276e2d41bbb68b323e566047a1bfbf952050b157d8b5cdc74c07c1bf4ca3b6" [[package]] name = "lock_api" @@ -787,7 +795,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -799,7 +807,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -811,7 +819,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de28bc5222f0f8495fdf5134dd91b7dc880bfdc1fa0646499d80c13d7ebbbd46" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "libpam-sys", "libpam-sys-helpers", @@ -950,9 +958,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" @@ -962,9 +970,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -1044,7 +1052,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -1098,9 +1106,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" +checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" dependencies = [ "bytecheck", "bytes", @@ -1117,9 +1125,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" +checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" dependencies = [ "proc-macro2", "quote", @@ -1130,7 +1138,7 @@ dependencies = [ name = "rootasrole" version = "4.0.0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bon", "capctl", "cbor4ii", @@ -1141,6 +1149,7 @@ dependencies = [ "glob", "hex", "indexmap", + "konst 0.4.3", "landlock", "libc", "libpam-sys", @@ -1167,7 +1176,7 @@ dependencies = [ name = "rootasrole-core" version = "4.0.0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bon", "capctl", "cbor4ii", @@ -1176,7 +1185,7 @@ dependencies = [ "env_logger", "glob", "indexmap", - "konst", + "konst 0.4.3", "libc", "log", "nix 0.30.1", @@ -1231,9 +1240,9 @@ checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -1284,9 +1293,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -1432,24 +1441,34 @@ dependencies = [ [[package]] name = "test-log" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4" +checksum = "2f46bf474f0a4afebf92f076d54fd5e63423d9438b8c278a3d2ccb0f47f7cdb3" dependencies = [ "test-log-macros", ] [[package]] -name = "test-log-macros" -version = "0.2.19" +name = "test-log-core" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" +checksum = "37d4d41320b48bc4a211a9021678fcc0c99569b594ea31c93735b8e517102b4c" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "test-log-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9beb9249a81e430dffd42400a49019bcf548444f1968ff23080a625de0d4d320" +dependencies = [ + "syn 2.0.117", + "test-log-core", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1544,39 +1563,30 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.2", ] [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "typewit" -version = "1.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" -dependencies = [ - "typewit_proc_macros", -] - -[[package]] -name = "typewit_proc_macros" -version = "1.8.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" [[package]] name = "ucd-trie" @@ -1604,9 +1614,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "js-sys", "wasm-bindgen", @@ -1620,18 +1630,18 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.115" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -1642,9 +1652,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.115" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1652,9 +1662,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.115" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", @@ -1665,9 +1675,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.115" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -1770,15 +1780,15 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "xtask" diff --git a/Cargo.toml b/Cargo.toml index 695aa59c..b4e7a254 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["xtask", "rar-common", "rar-exec"] [package] name = "rootasrole" version = "4.0.0" -rust-version = "1.88.0" +rust-version = "1.93.0" authors = ["Eddie Billoir "] edition = "2024" default-run = "dosr" @@ -60,7 +60,7 @@ ssd = [] hierarchy = [] timeout = [] landlock = ["dep:landlock", "dep:bitflags", "dep:glob"] -editor = ["dep:landlock", "dep:libseccomp", "dep:pest", "dep:pest_derive", "dep:indexmap"] +editor = ["dep:landlock", "dep:libseccomp", "dep:pest", "dep:pest_derive", "dep:indexmap", "dep:konst"] [lints.rust] unexpected_cfgs = "allow" @@ -68,9 +68,9 @@ unexpected_cfgs = "allow" [lints.clippy] pedantic = { level = "warn", priority = 1 } nursery = { level = "warn", priority = 1 } -unwrap_used = "deny" -similar_names = "allow" -should_implement_trait = "allow" +unwrap_used = { level = "deny", priority = 2 } +similar_names = { level = "allow", priority = 2 } +should_implement_trait = { level = "allow", priority = 2 } [package.metadata.clippy] allow-unwrap-in-tests = true @@ -106,8 +106,10 @@ pest_derive = { version = "2.7", default-features = false, features = ["std"], o indexmap = { version = "2.13", default-features = false, features = ["std"], optional = true } hex = { version = "0.4", default-features = false, optional = true, features = ["alloc"]} landlock = { version = "0.4", optional = true } -libseccomp = { version = "0.3", optional = true } +libseccomp = { version = "0.4", optional = true } bitflags = { version = "2.9", default-features = false, optional = true } +konst = { version= "0.4", default-features = false, optional = true, features = ["parsing_proc", "iter"] } + [dev-dependencies] log = { version = "0.4", default-features = false, features = ["std"] } diff --git a/rar-common/Cargo.toml b/rar-common/Cargo.toml index d8a000e8..59616f44 100644 --- a/rar-common/Cargo.toml +++ b/rar-common/Cargo.toml @@ -28,7 +28,7 @@ syslog = { version= "6.0", default-features = false } env_logger = { version= "0.11", default-features = false } bon = { version = "3", default-features = false, features = ["experimental-overwritable"] } cbor4ii = { version = "1.0", default-features = false, features = ["serde", "serde1", "use_std"] } -konst = { version= "0.3", default-features = false, features = ["parsing_proc", "iter"] } +konst = { version= "0.4", default-features = false, features = ["parsing_proc", "iter"] } [dev-dependencies] log = { version= "0.4", default-features = false } @@ -39,6 +39,13 @@ serde_test = "1.0" serde = { version = "1.0", default-features = false, features= ["rc", "derive"] } serde_json = { version= "1.0", default-features = false } +[lints.clippy] +pedantic = { level = "warn", priority = 1 } +nursery = { level = "warn", priority = 1 } +unwrap_used = { level = "deny", priority = 2 } +similar_names = { level = "allow", priority = 2 } +should_implement_trait = { level = "allow", priority = 2 } + [features] default = ["pcre2", "glob"] pcre2 = ["dep:pcre2"] diff --git a/rar-common/src/database/actor.rs b/rar-common/src/database/actor.rs index e398cd4d..16846f44 100644 --- a/rar-common/src/database/actor.rs +++ b/rar-common/src/database/actor.rs @@ -5,12 +5,12 @@ use std::{ use bon::bon; use log::debug; -use nix::unistd::{Group, User}; +use nix::unistd::{Gid, Group, User}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use strum::EnumIs; -use crate::util::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1}; +use crate::util::{Either, HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, either_to_gid}; #[derive(Serialize, Debug, EnumIs, Clone, PartialEq, Eq, strum::Display)] #[serde(untagged, rename_all = "lowercase")] @@ -576,17 +576,17 @@ impl PartialEq for SGroupType { } } -impl PartialEq for SGroupType { - fn eq(&self, other: &Group) -> bool { +impl PartialEq> for SGroupType { + fn eq(&self, other: &Either) -> bool { let gid = self.fetch_id(); - gid.is_some_and(|gid| gid == other.gid.as_raw()) + gid.is_some_and(|gid| gid == either_to_gid(other).as_raw()) } } -impl PartialEq for DGroupType<'_> { - fn eq(&self, other: &Group) -> bool { +impl PartialEq> for DGroupType<'_> { + fn eq(&self, other: &Either) -> bool { let gid = self.fetch_id(); - gid.is_some_and(|gid| gid == other.gid.as_raw()) + gid.is_some_and(|gid| gid == either_to_gid(other).as_raw()) } } @@ -797,12 +797,14 @@ impl SActor { impl core::fmt::Display for SActor { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::User { id, .. } => { - write!(f, "User: {}", id.as_ref().unwrap()) - } - Self::Group { groups, .. } => { - write!(f, "Group: {}", groups.as_ref().unwrap()) - } + Self::User { id, .. } => match id.as_ref() { + Some(id) => write!(f, "User: {id}"), + None => write!(f, "User: "), + }, + Self::Group { groups, .. } => match groups.as_ref() { + Some(groups) => write!(f, "Group: {groups}"), + None => write!(f, "Group: "), + }, Self::Unknown(unknown) => { write!(f, "Unknown: {unknown}") } @@ -1081,7 +1083,7 @@ mod tests { #[test] fn test_partialeq_group() { - let group = Group::from_gid(0.into()).unwrap().unwrap(); + let group = Either::Left(Group::from_gid(0.into()).unwrap().unwrap()); assert!(SGroupType::from(0) == group); assert!(SGroupType::from(1) != group); assert!(SGroupType::from("root") == group); diff --git a/rar-common/src/database/de.rs b/rar-common/src/database/de.rs index 95770b6f..957fdbb8 100644 --- a/rar-common/src/database/de.rs +++ b/rar-common/src/database/de.rs @@ -325,14 +325,12 @@ impl<'de> Deserialize<'de> for SSetgidSet { } } } - if fallback.is_none() { - return Err(de::Error::custom( - "Missing required field 'fallback' in SSetgidSet", - )); - } + let fallback = fallback.ok_or_else(|| { + de::Error::custom("Missing required field 'fallback' in SSetgidSet") + })?; Ok(SSetgidSet { default_behavior, - fallback: fallback.unwrap(), + fallback, add, sub, }) diff --git a/rar-common/src/database/mod.rs b/rar-common/src/database/mod.rs index 0be230ef..8249da2f 100644 --- a/rar-common/src/database/mod.rs +++ b/rar-common/src/database/mod.rs @@ -19,10 +19,10 @@ pub mod options; pub mod ser; pub mod structs; pub mod versionning; +pub mod warn; #[allow(clippy::missing_errors_doc)] #[derive(Debug, Default, Builder)] -#[builder(on(_, overwritable))] pub struct FilterMatcher { pub role: Option, pub task: Option, @@ -31,6 +31,7 @@ pub struct FilterMatcher { pub user: Option, #[builder(with = |s: impl Into| -> Result<_,String> { s.into().try_into() })] pub group: Option>, + pub workdir: Option, } // deserialize the linked hash set diff --git a/rar-common/src/database/options.rs b/rar-common/src/database/options.rs index 9dc88c3f..41657e66 100644 --- a/rar-common/src/database/options.rs +++ b/rar-common/src/database/options.rs @@ -12,6 +12,8 @@ use chrono::Duration; use indexmap::IndexSet; use konst::eq_str; +#[cfg(feature = "pcre2")] +use log::warn; use nix::sys::stat::Mode; #[cfg(feature = "pcre2")] use pcre2::bytes::Regex; @@ -33,7 +35,7 @@ use super::{FilterMatcher, deserialize_duration, is_default, serialize_duration} use super::{ lhs_deserialize, lhs_deserialize_envkey, lhs_serialize, lhs_serialize_envkey, - structs::{SConfig, SRole, STask}, + structs::{SPolicy, SRole, STask}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] @@ -47,7 +49,7 @@ pub enum Level { Task, } -#[derive(Debug, Clone, Copy, FromRepr, EnumIter, Display)] +#[derive(Debug, Clone, Copy, FromRepr, EnumIter, Display, EnumIs)] pub enum OptType { Path, Env, @@ -57,6 +59,7 @@ pub enum OptType { Authentication, ExecInfo, UMask, + Workdir, } #[derive( @@ -130,10 +133,6 @@ pub struct SPathOptions { pub sub: Option>, } -// ...existing code... -impl SPathOptions {} -// ...existing code... - #[derive( Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, )] @@ -212,6 +211,80 @@ pub struct SEnvOptions { pub extra_fields: Map, } +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +#[serde(untagged)] +pub enum SWorkdirEither { + /// This is the equivalent of deny all and fallback to the specified path. + Path(String), + Struct(SWorkdirSet), +} + +#[derive(Serialize, Hash, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Default)] +#[repr(u32)] +pub enum WorkdirBehavior { + #[serde(rename = "none")] + Allowlist = HARDENED_ENUM_VALUE_0, // Deny all except for the listed ones in "add" minus "sub" ofc + #[serde(rename = "all")] + Blacklist = HARDENED_ENUM_VALUE_1, // Allow all except for the listed ones in "sub" + #[default] + #[serde(rename = "inherit")] + Inherit = HARDENED_ENUM_VALUE_2, // Inherit from parent levels, which can be combined with the above two behaviors. +} + +impl WorkdirBehavior { + #[must_use] + /// # Panics + /// Panics if the input string does not match any of the valid ``PathBehavior`` variants. + pub const fn const_parse(input: &str) -> Self { + match input { + _ if eq_str(input, "all") => Self::Blacklist, + _ if eq_str(input, "none") => Self::Allowlist, + _ if eq_str(input, "inherit") => Self::Inherit, + _ => panic!("fail to parse WorkdirBehavior"), + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)] +pub struct SWorkdirSet { + /// The default behavior for workdir handling. This determines how the "add" and "sub" lists are interpreted. + /// - If set to `Allowlist`, only the paths in the "add" list (minus those in the "sub" list) will be allowed as workdirs. + /// - If set to `Blacklist`, all paths will be allowed as workdirs except those in the "sub" list. + /// - If set to `Inherit`, the behavior will be inherited from parent levels, which can be combined with the above two behaviors. + /// + /// Note: The target user must have permissions to access the allowed workdirs, otherwise the command will fail to execute. + /// If you want bypass the access control check, grant the `CAP_DAC_READ_SEARCH` capability in the "cred" section + #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[builder(start_fn)] + pub default_behavior: WorkdirBehavior, + + /// The "fallback" field specifies a fallback directory to use as the working directory. + /// This will override the current user working directory. + /// For example: + /// someone type: `dosr ls` in his home directory, but the config has a fallback of `/tmp`, + /// then the command will be executed with `/tmp` as the working directory instead of the user's home directory. + /// This is useful in scenarios where users do not have to know or care about the actual working directory of a command + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fallback: Option, + + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "lhs_deserialize", + serialize_with = "lhs_serialize" + )] + #[builder(with = |v : impl IntoIterator| { v.into_iter().map(|s| s.to_string()).collect() })] + pub add: Option>, + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "lhs_deserialize", + serialize_with = "lhs_serialize", + alias = "del" + )] + #[builder(with = |v : impl IntoIterator| { v.into_iter().map(|s| s.to_string()).collect() })] + pub sub: Option>, +} #[derive( Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, )] @@ -341,19 +414,21 @@ pub enum SInfo { pub struct Opt { #[serde(skip)] pub level: Level, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub path: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub env: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub root: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub bounding: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub authentication: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub execinfo: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub workdir: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub umask: Option, @@ -372,6 +447,7 @@ impl Opt { bounding: Option, authentication: Option, execinfo: Option, + workdir: Option, timeout: Option, umask: Option, #[builder(default)] extra_fields: Map, @@ -384,6 +460,7 @@ impl Opt { bounding, authentication, execinfo, + workdir, timeout, umask, extra_fields, @@ -403,14 +480,15 @@ impl Opt { .execinfo(INFO) .umask(UMASK) .env( + #[allow(clippy::unwrap_used)] SEnvOptions::builder(ENV_DEFAULT_BEHAVIOR) - .keep(ENV_KEEP_LIST) + .keep(ENV_KEEP_LIST.iter().copied()) .unwrap() - .check(ENV_CHECK_LIST) + .check(ENV_CHECK_LIST.iter().copied()) .unwrap() - .delete(ENV_DELETE_LIST) + .delete(ENV_DELETE_LIST.iter().copied()) .unwrap() - .set(ENV_SET_LIST) + .set(ENV_SET_LIST.iter().copied()) .override_behavior(ENV_OVERRIDE_BEHAVIOR) .build(), ) @@ -538,15 +616,25 @@ impl EnvSet for Option> { } #[cfg(feature = "pcre2")] -fn check_wildcarded(wildcarded: &EnvKey, s: &String) -> bool { - Regex::new(&format!("^{}$", wildcarded.value)) // convert to regex - .unwrap() - .is_match(s.as_bytes()) - .is_ok_and(|m| m) +fn check_wildcarded(wildcarded: &EnvKey, s: &str) -> bool { + let pattern = format!("^{}$", wildcarded.value); + match Regex::new(&pattern) { + Ok(regex) => match regex.is_match(s.as_bytes()) { + Ok(is_match) => is_match, + Err(err) => { + warn!("Regex match error for '{pattern}': {err}"); + false + } + }, + Err(err) => { + warn!("Invalid regex '{pattern}': {err}"); + false + } + } } #[cfg(not(feature = "pcre2"))] -fn check_wildcarded(_wildcarded: &EnvKey, _s: &String) -> bool { +fn check_wildcarded(_wildcarded: &EnvKey, _s: &str) -> bool { true } @@ -668,10 +756,11 @@ impl Default for Opt { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] //Maybe used for other binaries +#[derive(Debug, Clone)] pub struct OptStack { pub(crate) stack: [Option>>; 5], - roles: Option>>, + roles: Option>>, role: Option>>, task: Option>>, } @@ -701,9 +790,9 @@ impl OptStackBuilder { .borrow() .role .as_ref() - .unwrap() + .expect("task must belong to a role") .upgrade() - .unwrap(), + .expect("role should not be dropped before task"), ) .task(task.clone()) .opt(task.as_ref().borrow().options.clone()) @@ -722,9 +811,9 @@ impl OptStackBuilder { .borrow() .config .as_ref() - .unwrap() + .expect("role must belong to a config") .upgrade() - .unwrap(), + .expect("config should not be dropped before role"), ) .role(role.clone()) .opt(role.as_ref().borrow().options.clone()) @@ -732,7 +821,7 @@ impl OptStackBuilder { fn with_roles( self, - roles: &Rc>, + roles: &Rc>, ) -> OptStackBuilder> where ::Roles: opt_stack_builder::IsUnset, @@ -752,7 +841,7 @@ impl OptStack { #[builder] pub const fn new( #[builder(field)] stack: [Option>>; 5], - roles: Option>>, + roles: Option>>, role: Option>>, task: Option>>, ) -> Self { @@ -769,7 +858,7 @@ impl OptStack { pub fn from_role(role: &Rc>) -> Self { Self::builder().with_role(role).build() } - pub fn from_roles(roles: &Rc>) -> Self { + pub fn from_roles(roles: &Rc>) -> Self { Self::builder().with_roles(roles).build() } @@ -861,7 +950,7 @@ impl OptStack { .build() } - fn get_final_env(&self, cmd_filter: Option<&FilterMatcher>) -> SEnvOptions { + fn get_final_env(&self, cmd_filter: Option<&FilterMatcher>) -> Result { let mut final_behavior = EnvBehavior::default(); let mut final_set = HashMap::new(); let mut final_keep = IndexSet::new(); @@ -922,15 +1011,15 @@ impl OptStack { }; } }); - SEnvOptions::builder(overriden_behavior.unwrap_or(final_behavior)) + let builder = SEnvOptions::builder(overriden_behavior.unwrap_or(final_behavior)) .set(final_set) .keep(final_keep) - .unwrap() + .map_err(|err| format!("Failed to set env keep list: {err}"))? .check(final_check) - .unwrap() + .map_err(|err| format!("Failed to set env check list: {err}"))? .delete(final_delete) - .unwrap() - .build() + .map_err(|err| format!("Failed to set env delete list: {err}"))?; + Ok(builder.build()) } fn get_level(&self) -> Level { @@ -940,12 +1029,13 @@ impl OptStack { level } - #[must_use] - pub fn to_opt(&self) -> Rc> { - rc_refcell!( + /// # Errors + /// Returns an error if any environment option builder step fails. + pub fn to_opt(&self) -> Result>, String> { + Ok(rc_refcell!( Opt::builder(self.get_level()) .path(self.get_final_path()) - .env(self.get_final_env(None)) + .env(self.get_final_env(None)?) .maybe_root( self.find_in_options(|opt| opt.root.map(|root| (opt.level, root))) .map(|(_, root)| root), @@ -969,7 +1059,7 @@ impl OptStack { .map(|(_, timeout)| timeout), ) .build() - ) + )) } } @@ -1012,7 +1102,7 @@ mod tests { #[test] fn test_find_in_options() { - let config = SConfig::builder() + let config = SPolicy::builder() .role( SRole::builder("test") .options(|opt| { @@ -1050,7 +1140,7 @@ mod tests { #[test] fn test_env_global_to_task() { - let config = SConfig::builder() + let config = SPolicy::builder() .role( SRole::builder("test") .task( @@ -1087,7 +1177,9 @@ mod tests { .build() }) .build(); - let binding = OptStack::from_task(&config.task("test", 1).unwrap()).to_opt(); + let binding = OptStack::from_task(&config.task("test", 1).unwrap()) + .to_opt() + .expect("Failed to build task options"); let options = binding.as_ref().borrow(); let res = &options.env.as_ref().unwrap().keep; assert!( @@ -1102,7 +1194,7 @@ mod tests { #[allow(clippy::too_many_lines)] #[test] fn test_to_opt() { - let config = SConfig::builder() + let config = SPolicy::builder() .role( SRole::builder("test") .task( @@ -1183,7 +1275,7 @@ mod tests { .build(); let default = IndexSet::new(); let stack = OptStack::from_roles(&config); - let opt = stack.to_opt(); + let opt = stack.to_opt().expect("Failed to build global options"); let global_options = opt.as_ref().borrow(); assert_eq!( global_options.path.as_ref().unwrap().default_behavior, @@ -1243,7 +1335,9 @@ mod tests { global_options.timeout.as_ref().unwrap().type_field.unwrap(), TimestampType::TTY ); - let opt = OptStack::from_role(&config.role("test").unwrap()).to_opt(); + let opt = OptStack::from_role(&config.role("test").unwrap()) + .to_opt() + .expect("Failed to build role options"); let role_options = opt.as_ref().borrow(); assert_eq!( role_options.path.as_ref().unwrap().default_behavior, @@ -1287,7 +1381,9 @@ mod tests { role_options.timeout.as_ref().unwrap().type_field.unwrap(), TimestampType::PPID ); - let opt = OptStack::from_task(&config.task("test", 1).unwrap()).to_opt(); + let opt = OptStack::from_task(&config.task("test", 1).unwrap()) + .to_opt() + .expect("Failed to build task options"); let task_options = opt.as_ref().borrow(); assert_eq!( task_options.path.as_ref().unwrap().default_behavior, @@ -1349,7 +1445,7 @@ mod tests { #[test] fn test_get_final_env_set_inherit() { - let config = SConfig::builder() + let config = SPolicy::builder() .role( SRole::builder("test") .task( @@ -1384,7 +1480,7 @@ mod tests { }) .build(); let stack = OptStack::from_task(&config.task("test", 1).unwrap()); - let opt = stack.to_opt(); + let opt = stack.to_opt().expect("Failed to build task options"); let options = opt.as_ref().borrow(); assert_eq!( options @@ -1403,7 +1499,7 @@ mod tests { #[test] fn test_get_final_path_inherit() { - let config = SConfig::builder() + let config = SPolicy::builder() .role( SRole::builder("test") .task( @@ -1438,7 +1534,7 @@ mod tests { }) .build(); let stack = OptStack::from_task(&config.task("test", 1).unwrap()); - let opt = stack.to_opt(); + let opt = stack.to_opt().expect("Failed to build task options"); let options = opt.as_ref().borrow(); assert!( options @@ -1474,7 +1570,7 @@ mod tests { #[test] fn test_find_in_options_none() { - let config = SConfig::builder() + let config = SPolicy::builder() .role( SRole::builder("test") .task(STask::builder(1).build()) diff --git a/rar-common/src/database/score.rs b/rar-common/src/database/score.rs index 22a94518..77de947e 100644 --- a/rar-common/src/database/score.rs +++ b/rar-common/src/database/score.rs @@ -28,6 +28,16 @@ pub enum HardenedBool { True = HARDENED_ENUM_VALUE_1, } +#[inline] +#[must_use] +pub const fn hardened_bool_from_bool(b: bool) -> HardenedBool { + if b { + HardenedBool::True + } else { + HardenedBool::False + } +} + impl ActorMatchMin { #[inline] #[must_use] diff --git a/rar-common/src/database/ser.rs b/rar-common/src/database/ser.rs index 4fa922e5..0cf4328f 100644 --- a/rar-common/src/database/ser.rs +++ b/rar-common/src/database/ser.rs @@ -8,12 +8,12 @@ use crate::util::optimized_serialize_capset; use super::{ is_default, structs::{ - SCapabilities, SCommands, SConfig, SCredentials, SRole, SSetgidSet, SSetuidSet, STask, + SCapabilities, SCommands, SCredentials, SPolicy, SRole, SSetgidSet, SSetuidSet, STask, SetBehavior, cmds_is_default, }, }; -impl Serialize for SConfig { +impl Serialize for SPolicy { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -386,7 +386,7 @@ mod tests { #[test] fn test_sconfig_human_readable() { - let config = SConfig { + let config = SPolicy { options: Some(Rc::default()), roles: vec![], extra_fields: Map::default(), diff --git a/rar-common/src/database/structs.rs b/rar-common/src/database/structs.rs index 7edb256d..5ff389f7 100644 --- a/rar-common/src/database/structs.rs +++ b/rar-common/src/database/structs.rs @@ -23,8 +23,8 @@ use super::{ options::{Level, Opt, OptBuilder}, }; -#[derive(Deserialize, PartialEq, Eq, Debug, Default)] -pub struct SConfig { +#[derive(Deserialize, PartialEq, Eq, Debug, Default, Clone)] +pub struct SPolicy { #[serde(default, deserialize_with = "sconfig_opt", alias = "o")] pub options: Option>>, #[serde(default, alias = "r")] @@ -68,7 +68,7 @@ pub struct SRole { pub extra_fields: Map, #[serde(skip)] #[derivative(PartialEq = "ignore")] - pub config: Option>>, + pub config: Option>>, } fn srole_opt<'de, D>(deserializer: D) -> Result>>, D::Error> @@ -386,7 +386,7 @@ impl From for SCapabilities { // Implementations for Struct navigation // ======================== #[bon] -impl SConfig { +impl SPolicy { #[builder] pub fn new( #[builder(field)] roles: Vec>>, @@ -421,7 +421,7 @@ pub trait TaskGetter { fn task(&self, name: &IdTask) -> Option>>; } -impl RoleGetter for Rc> { +impl RoleGetter for Rc> { fn role(&self, name: &str) -> Option>> { self.as_ref() .borrow() @@ -453,7 +453,7 @@ impl TaskGetter for Rc> { } } -impl SConfigBuilder { +impl SPolicyBuilder { pub fn role(mut self, role: Rc>) -> Self { self.roles.push(role); self @@ -508,7 +508,7 @@ impl SRole { s } #[must_use] - pub fn config(&self) -> Option>> { + pub fn config(&self) -> Option>> { self.config.as_ref()?.upgrade() } #[must_use] @@ -548,7 +548,7 @@ impl STask { } } -impl Index for SConfig { +impl Index for SPolicy { type Output = Rc>; fn index(&self, index: usize) -> &Self::Output { @@ -692,7 +692,7 @@ mod tests { ] } "#; - let config: SConfig = serde_json::from_str(config).unwrap(); + let config: SPolicy = serde_json::from_str(config).unwrap(); let options = config.options.as_ref().unwrap().as_ref().borrow(); let path = options.path.as_ref().unwrap(); assert_eq!(path.default_behavior, PathBehavior::Delete); @@ -847,7 +847,7 @@ mod tests { "unknown": "unknown" } "#; - let config: SConfig = serde_json::from_str(config).unwrap(); + let config: SPolicy = serde_json::from_str(config).unwrap(); assert_eq!(config.extra_fields.get("unknown").unwrap(), "unknown"); let binding = config.options.unwrap(); @@ -951,7 +951,7 @@ mod tests { ] } "#; - let config: SConfig = serde_json::from_str(config).unwrap(); + let config: SPolicy = serde_json::from_str(config).unwrap(); let options = config.options.as_ref().unwrap().as_ref().borrow(); let path = options.path.as_ref().unwrap(); assert_eq!(path.default_behavior, PathBehavior::Delete); @@ -1029,7 +1029,7 @@ mod tests { #[test] fn test_serialize() { - let config = SConfig::builder() + let config = SPolicy::builder() .role( SRole::builder("role1") .actor(SActor::user("user1").build()) @@ -1103,7 +1103,7 @@ mod tests { #[test] fn test_serialize_operride_behavior_option() { - let config = SConfig::builder() + let config = SPolicy::builder() .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Inherit) diff --git a/rar-common/src/database/versionning.rs b/rar-common/src/database/versionning.rs index e51f262a..f952348f 100644 --- a/rar-common/src/database/versionning.rs +++ b/rar-common/src/database/versionning.rs @@ -2,11 +2,11 @@ use semver::Version; use serde::{Deserialize, Serialize}; use std::fmt::Debug; -use super::migration::Migration; -use crate::{FullSettings, PACKAGE_VERSION}; +use crate::{PACKAGE_VERSION, database::migration::Migration}; #[derive(Deserialize, Serialize, Debug)] pub struct Versioning { + #[serde(alias = "v")] pub version: Version, #[serde(default, flatten)] pub data: T, @@ -30,4 +30,15 @@ impl Default for Versioning { } } -pub(crate) const SETTINGS_MIGRATIONS: &[Migration] = &[]; +impl Versioning { + /// # Errors + /// Returns an error if the migration process fails. + pub fn upgrade_version( + &mut self, + migrations: &[Migration], + ) -> Result> { + let res = Migration::migrate(&self.version, &mut self.data, migrations)?; + self.version = PACKAGE_VERSION; + Ok(res) + } +} diff --git a/rar-common/src/database/warn.rs b/rar-common/src/database/warn.rs new file mode 100644 index 00000000..d3fad913 --- /dev/null +++ b/rar-common/src/database/warn.rs @@ -0,0 +1,407 @@ +use crate::{ + SettingsContent, + database::{ + actor::{SActor, SGroups}, + structs::{ + SCommand, SCommands, SCredentials, SGroupsEither, SPolicy, SRole, STask, SUserEither, + SetBehavior, + }, + }, + file::RootSettings, +}; + +#[allow(dead_code)] +pub trait Warn { + fn warn_anomalies(&self, warn: F) + where + F: FnMut(String); +} + +impl Warn for RootSettings { + fn warn_anomalies(&self, mut warn: F) + where + F: FnMut(String), + { + self.storage.warn_anomalies(&mut warn); + if let Some(config) = &self.config { + config.as_ref().borrow().warn_anomalies(&mut warn); + } else if self + .storage + .settings + .as_ref() + .is_none_or(|f| f.path.is_none()) + { + warn("Warning: No configuration section found".to_string()); + } + } +} + +impl Warn for SPolicy { + fn warn_anomalies(&self, mut warn: F) + where + F: FnMut(String), + { + for key in self.extra_fields.keys() { + warn(format!("Warning: Unknown configuration field '{key}'")); + } + if let Some(opt) = &self.options { + for key in opt.as_ref().borrow().extra_fields.keys() { + warn(format!( + "Warning: Unknown options field at {:?} level '{}'", + opt.as_ref().borrow().level, + key + )); + } + } + for role in &self.roles { + role.as_ref().borrow().warn_anomalies(&mut warn); + } + } +} + +impl Warn for SRole { + #[allow(clippy::too_many_lines)] + fn warn_anomalies(&self, mut warn: F) + where + F: FnMut(String), + { + for key in self.extra_fields.keys() { + warn(format!( + "Warning: Unknown role field in role '{}' : '{}'", + self.name, key + )); + } + self.warn_actors(&mut warn); + if let Some(opt) = &self.options { + for key in opt.as_ref().borrow().extra_fields.keys() { + warn(format!( + "Warning: Unknown options field at {:?} level in role '{}' : '{}'", + opt.as_ref().borrow().level, + self.name, + key + )); + } + } + for task in &self.tasks { + for key in self.extra_fields.keys() { + warn(format!( + "Warning: Unknown task field in role '{}' task '{:?}' : '{}'", + task.as_ref().borrow().name, + self.name, + key + )); + } + warn_cred( + self, + &task.as_ref().borrow(), + &task.as_ref().borrow().cred, + &mut warn, + ); + warn_cmds( + self, + &task.as_ref().borrow(), + &task.as_ref().borrow().commands, + &mut warn, + ); + if let Some(opt) = &self.options { + for key in opt.as_ref().borrow().extra_fields.keys() { + warn(format!( + "Warning: Unknown options field at {:?} level in role '{}' task '{:?}' : '{}'", + opt.as_ref().borrow().level, + self.name, + self.name, + key + )); + } + } + } + } +} + +impl SRole { + fn warn_actors(&self, mut warn: F) + where + F: FnMut(String), + { + for actor in &self.actors { + if actor.is_unknown() { + warn(format!( + "Warning: Unknown actor type in role '{}' : '{:?}'", + self.name, actor + )); + } else if let SActor::User { id, extra_fields } = actor { + if let Some(id) = id + && id.fetch_user().is_none() + { + warn(format!( + "Warning: Unknown user in role '{}' : '{}'", + self.name, id + )); + } + for key in extra_fields.keys() { + warn(format!( + "Warning: Unknown user field in role '{}' for user '{:?}' : '{}'", + self.name, id, key + )); + } + } else if let SActor::Group { + groups, + extra_fields, + } = actor + { + for key in extra_fields.keys() { + warn(format!( + "Warning: Unknown group field in role '{}' for group '{:?}' : '{}'", + self.name, groups, key + )); + } + if let Some(groups) = groups { + match groups { + SGroups::Single(sgroup_type) => { + if sgroup_type.fetch_group().is_none() { + warn(format!( + "Warning: Unknown group in role '{}' : '{:?}'", + self.name, sgroup_type + )); + } + } + SGroups::Multiple(sgroup_types) => { + for sgroup_type in sgroup_types { + if sgroup_type.fetch_group().is_none() { + warn(format!( + "Warning: Unknown group in role '{}' : '{:?}'", + self.name, sgroup_type + )); + } + } + } + } + } else { + warn(format!( + "Warning: No group specified in role '{}' : '{:?}'", + self.name, groups + )); + } + } + } + } +} + +#[allow(clippy::too_many_lines)] +fn warn_cred(role: &SRole, task: &STask, cred: &SCredentials, mut warn: F) +where + F: FnMut(String), +{ + for key in cred.extra_fields.keys() { + warn(format!( + "Warning: Unknown cred field in role '{}' task '{:?}' : '{}'", + role.name, task.name, key + )); + } + if let Some(id) = &cred.setuid { + match id { + SUserEither::MandatoryUser(suser_type) => { + if suser_type.fetch_user().is_none() { + warn(format!( + "Warning: Unknown user in role '{}' task '{:?}' setuid: '{:?}'", + role.name, task.name, suser_type + )); + } + } + SUserEither::UserSelector(ssetuid_set) => { + if let Some(default) = &ssetuid_set.fallback + && default.fetch_user().is_none() + { + warn(format!( + "Warning: Unknown user in role '{}' task '{:?}' setuid fallback: '{:?}'", + role.name, task.name, default + )); + } + for add in &ssetuid_set.add { + if add.fetch_user().is_none() { + warn(format!( + "Warning: Unknown user in role '{}' task '{:?}' setuid add: '{:?}'", + role.name, task.name, add + )); + } + } + for sub in &ssetuid_set.sub { + if sub.fetch_user().is_none() { + warn(format!( + "Warning: Unknown user in role '{}' task '{:?}' setuid sub: '{:?}'", + role.name, task.name, sub + )); + } + } + } + } + } + if let Some(sgroups_either) = &cred.setgid { + match sgroups_either { + SGroupsEither::MandatoryGroup(group) => { + if group.fetch_group().is_none() { + warn(format!( + "Warning: Unknown group in role '{}' task '{:?}' setgid: '{:?}'", + role.name, task.name, group + )); + } + } + SGroupsEither::MandatoryGroups(sgroups) => match sgroups { + SGroups::Single(sgroup_type) => { + if sgroup_type.fetch_group().is_none() { + warn(format!( + "Warning: Unknown group in role '{}' task '{:?}' setgid: '{:?}'", + role.name, task.name, sgroup_type + )); + } + } + SGroups::Multiple(sgroup_types) => { + for sgroup_type in sgroup_types { + if sgroup_type.fetch_group().is_none() { + warn(format!( + "Warning: Unknown group in role '{}' task '{:?}' setgid: '{:?}'", + role.name, task.name, sgroup_type + )); + } + } + } + }, + SGroupsEither::GroupSelector(chooser) => { + match &chooser.fallback { + SGroups::Single(sgroup_type) => { + if sgroup_type.fetch_group().is_none() { + warn(format!( + "Warning: Unknown group in role '{}' task '{:?}' setgid fallback: '{:?}'", + role.name, task.name, sgroup_type + )); + } + } + SGroups::Multiple(sgroup_types) => { + for sgroup_type in sgroup_types { + if sgroup_type.fetch_group().is_none() { + warn(format!( + "Warning: Unknown group in role '{}' task '{:?}' setgid fallback: '{:?}'", + role.name, task.name, sgroup_type + )); + } + } + } + } + chooser.add.iter().for_each(|group| { + match group { + SGroups::Single(sgroup_type) => { + if sgroup_type.fetch_group().is_none() { + warn(format!( + "Warning: Unknown group in role '{}' task '{:?}' setgid add: '{:?}'", + role.name, + task.name, + sgroup_type + )); + } + } + SGroups::Multiple(sgroup_types) => { + for sgroup_type in sgroup_types { + if sgroup_type.fetch_group().is_none() { + warn(format!("Warning: Unknown group in role '{}' task '{:?}' setgid add: '{:?}'", role.name, task.name, sgroup_type)); + } + } + } + } + }); + chooser.sub.iter().for_each(|group| { + match group { + SGroups::Single(sgroup_type) => { + if sgroup_type.fetch_group().is_none() { + warn(format!( + "Warning: Unknown group in role '{}' task '{:?}' setgid sub: '{:?}'", + role.name, + task.name, + sgroup_type + )); + } + } + SGroups::Multiple(sgroup_types) => { + for sgroup_type in sgroup_types { + if sgroup_type.fetch_group().is_none() { + warn(format!("Warning: Unknown group in role '{}' task '{:?}' setgid sub: '{:?}'", role.name, task.name, sgroup_type)); + } + } + } + } + }); + } + } + } +} + +fn warn_cmds(role: &SRole, task: &STask, cmds: &SCommands, warn: &mut F) +where + F: FnMut(String), +{ + cmds.extra_fields.keys().for_each(|key| { + warn(format!( + "Warning: Unknown commands field in role '{}' task '{:?}' : '{}'", + role.name, task.name, key + )); + }); + if cmds.add.is_empty() + && !cmds + .default + .as_ref() + .is_some_and(|b| *b == SetBehavior::All) + { + warn(format!( + "Warning: No commands can be performed in role '{}' task '{:?}'", + role.name, task.name + )); + } + for cmd in &cmds.add { + match cmd { + SCommand::Simple(cmd) => { + if cmd.is_empty() { + warn(format!( + "Warning: Empty command in role '{}' task '{:?}' in add list", + role.name, task.name + )); + } + } + SCommand::Complex(value) => { + if value.as_object().is_none() { + warn(format!( + "Warning: Complex command is not an dictionnary in role '{}' task '{:?}' : '{:?}'", + role.name, task.name, value + )); + } + } + } + } + for cmd in &cmds.sub { + match cmd { + SCommand::Simple(cmd) => { + if cmd.is_empty() { + warn(format!( + "Warning: Empty command in role '{}' task '{:?}' in sub list", + role.name, task.name + )); + } + } + SCommand::Complex(value) => { + if value.as_object().is_none() { + warn(format!( + "Warning: Complex command is not an dictionnary in role '{}' task '{:?}' : '{:?}'", + role.name, task.name, value + )); + } + } + } + } +} + +impl Warn for SettingsContent { + fn warn_anomalies(&self, _warn: F) + where + F: FnMut(String), + { + // No anomalies possible for now. + } +} diff --git a/rar-common/src/file.rs b/rar-common/src/file.rs new file mode 100644 index 00000000..cf2357a1 --- /dev/null +++ b/rar-common/src/file.rs @@ -0,0 +1,1196 @@ +use std::{ + borrow::Cow, + cell::RefCell, + error::Error, + fmt::Debug, + fs::{File, OpenOptions}, + io::{self, BufReader, Seek}, + path::{Path, PathBuf}, + rc::Rc, +}; + +use bon::Builder; +use capctl::Cap; +use log::{debug, error, warn}; +use nix::fcntl::Flock; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; + +use crate::{ + SettingsContent, + database::{migration::Migration, structs::SPolicy, versionning::Versioning}, + util::{ + RAR_CFG_IMMUTABLE, RAR_CFG_PATH, RAR_CFG_TYPE, StorageMethod, has_privileges, is_immutable, + open_lock_with_privileges, read_with_privileges, with_mutable_config, write_config, + }, +}; + +pub const ROOT_MIGRATIONS: &[Migration] = &[]; +pub const POLICY_MIGRATIONS: &[Migration>>] = &[]; + +#[derive(Debug)] +pub struct LockedSettingsFile { + path: PathBuf, + fd: Flock, // file descriptor to the opened file, to keep the lock + pub data: T, +} + +/// This opens, deserialize and locks a settings file, and keeps the file descriptor open to keep the lock +/// it allows to save the settings file later +impl LockedSettingsFile { + /// # Errors + /// Returns an error if the file cannot be opened, deserialized or locked + pub fn open_read( + path: S, + data_loader: impl Fn(&S, &File) -> io::Result, + ) -> std::io::Result + where + S: AsRef, + { + Self::open(path, OpenOptions::new().read(true), false, data_loader) + } + /// # Errors + /// Returns an error if the file cannot be opened, deserialized, locked or written to + pub fn open_write( + path: S, + data_loader: impl Fn(&S, &File) -> io::Result, + ) -> std::io::Result + where + S: AsRef, + { + Self::open( + path, + OpenOptions::new().read(true).write(true).create(true), + true, + data_loader, + ) + } + + /// # Errors + /// Returns an error if the file cannot be opened, deserialized or locked + pub fn open( + path: S, + options: &std::fs::OpenOptions, + write: bool, + data_loader: impl Fn(&S, &File) -> io::Result, + ) -> std::io::Result + where + S: AsRef, + { + let load_data = || -> io::Result { + let file = open_lock_with_privileges( + path.as_ref(), + options, + nix::fcntl::FlockArg::LockExclusive, + )?; + + Ok(Self { + path: path.as_ref().to_path_buf(), + data: data_loader(&path, &file)?, + fd: file, + }) + }; + + if write && path.as_ref().exists() { + let mut file = read_with_privileges(&path)?; + if is_immutable(&file)? { + return with_mutable_config(&mut file, |_| load_data()); + } + } + + load_data() + } + + /// # Errors + /// Returns an error if the file cannot be written + /// due to a lock, permission error or writing error + pub fn save(&mut self, method: StorageMethod, immutable: bool) -> Result<(), Box> { + let immuable = immutable && has_privileges(&[Cap::LINUX_IMMUTABLE])?; + debug!("Settings file immutable: {immuable}"); + if immuable { + debug!("Toggling immutable off for config file"); + with_mutable_config(&mut self.fd, |file| { + debug!("Toggled immutable off for config file"); + file.rewind()?; + file.set_len(0)?; + write_config(&self.data, file, method) + }) + .map_err(|e| format!("Failed to write config file: {e}"))?; + } else { + let file = &mut *self.fd; + debug!("Writing config file"); + file.rewind()?; + debug!("Rewound config file for writing"); + file.set_len(0)?; + debug!("Truncated config file"); + write_config(&self.data, file, method)?; + // clear the rest of the file if any + debug!("Wrote config file"); + } + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, Builder, PartialEq, Eq)] +pub struct RootSettings { + pub storage: SettingsContent, + #[serde(flatten)] + pub config: Option>>, +} + +pub type ConfigMap = Vec; +pub type LockedPolicy = LockedSettingsFile>>>; +pub type LockedRootSettings = LockedSettingsFile>; + +#[derive(Builder)] +pub struct FileSettings { + #[builder(default)] + map: ConfigMap, + ///# Errors + /// Returns an error if any of the files cannot be opened or locked + #[builder(with = |path: PathBuf,config : RootSettings| -> Result<_, Box> { + Ok(LockedSettingsFile::open_write(path, |_, _| { + let versioned: Versioning = Versioning::new(config.clone()); + Ok(versioned) + })?) + })] + root: LockedRootSettings, +} + +impl FileSettings { + /// # Errors + /// Returns an error if any of the files cannot be opened, deserialized or locked + pub fn read_all

( + rar_cfg_path: P, + rar_cfg_data_path: P, + rar_cfg_type: StorageMethod, + ) -> std::io::Result + where + P: AsRef, + { + Self::load_all( + rar_cfg_path, + rar_cfg_data_path, + rar_cfg_type, + OpenOptions::new().read(true), + false, + ) + } + /// # Errors + /// Returns an error if any of the files cannot be opened, deserialized, locked or written to + pub fn write_all

( + rar_cfg_path: P, + rar_cfg_data_path: P, + rar_cfg_type: StorageMethod, + ) -> std::io::Result + where + P: AsRef, + { + Self::load_all( + rar_cfg_path, + rar_cfg_data_path, + rar_cfg_type, + OpenOptions::new().read(true).write(true).create(true), + true, + ) + } + + /// # Errors + /// Returns an error if any of the files cannot be opened, deserialized or locked + pub fn read_policy

(cfg_path: P, cfg_type: StorageMethod) -> std::io::Result + where + P: AsRef, + { + Self::load_policy_file(cfg_type, OpenOptions::new().read(true), false, cfg_path) + } + /// # Errors + /// Returns an error if any of the files cannot be opened, deserialized, locked or written to + pub fn write_policy

(cfg_path: P, cfg_type: StorageMethod) -> std::io::Result + where + P: AsRef, + { + Self::load_policy_file( + cfg_type, + OpenOptions::new().read(true).write(true).create(true), + true, + cfg_path, + ) + } + /// # Errors + /// Returns an error if any of the files cannot be opened, deserialized or locked + fn load_all

( + rar_cfg_path: P, + rar_cfg_data_path: P, + rar_cfg_type: StorageMethod, + options: &OpenOptions, + write: bool, + ) -> std::io::Result + where + P: AsRef, + { + let rar_cfg_data_path = rar_cfg_data_path.as_ref().to_path_buf(); + let mut root = + LockedSettingsFile::open(rar_cfg_path.as_ref(), options, write, |path, file| { + debug!("Loading root settings from {}", path.display()); + let mut settings: Versioning = match rar_cfg_type { + StorageMethod::JSON => { + serde_json::from_reader(file).inspect_err(|e| debug!("{e}"))? + } + StorageMethod::CBOR => cbor4ii::serde::from_reader(BufReader::new(file)) + .map_err(|e| { + debug!("Failed to deserialize root settings: {e}"); + io::Error::new(io::ErrorKind::InvalidData, e) + })?, + }; + if let Some(config) = settings.data.config.as_ref() { + Self::make_weak_config(config); + } + settings.upgrade_version(ROOT_MIGRATIONS).map_err(|e| { + debug!("Failed to upgrade root settings: {e}"); + io::Error::other(e.to_string()) + })?; + debug!("Loaded root settings from {}", path.display()); + Ok(settings) + })?; + let mut map = ConfigMap::new(); + + if let Some(path) = root + .data + .data + .storage + .settings + .as_ref() + .and_then(|settings| settings.path.as_ref()) + .and_then(|path| { + if path.as_path() == rar_cfg_path.as_ref() { + None + } else { + Some(path.clone()) + } + }) + .or_else(|| { + if rar_cfg_path.as_ref() == rar_cfg_data_path { + None + } else { + Some(rar_cfg_data_path) + } + }) + { + if root.data.data.config.is_some() { + warn!( + "A policy has been detected in {}, but a different path is specified. + Ignoring the policy and keeping only the ones in the specified path: {}", + rar_cfg_path.as_ref().display(), + path.display() + ); + root.data.data.config = None; + } + if path.is_dir() { + debug!("Loading settings from directory {}", path.display()); + for entry in std::fs::read_dir(path)? { + let entry = entry?; + if entry.file_type()?.is_file() { + let path = entry.path(); + debug!("Loading settings from file {}", path.display()); + let config = Self::load_policy_file( + root.data.data.storage.method, + options, + write, + &path, + ); + match config { + Ok(config) => { + debug!("Loaded settings from file {}", path.display()); + Self::make_weak_config(&config.data.data); + map.push(config); + } + Err(e) => debug!( + "Failed to load settings from file {}: {}", + path.display(), + e + ), + } + } + } + } else if path.is_file() { + debug!("Loading settings from file {}", path.display()); + let config = + Self::load_policy_file(root.data.data.storage.method, options, write, &path)?; + debug!("Loaded settings from file {}", path.display()); + map.push(config); + } + } + + Ok(Self { map, root }) + } + + /// # Errors + /// Returns an error if the file cannot be opened or deserialized + fn load_policy_file

( + file_type: StorageMethod, + options: &OpenOptions, + write: bool, + path: P, + ) -> std::io::Result + where + P: AsRef, + { + let mut policyfile: LockedPolicy = + LockedSettingsFile::open(path, options, write, |_, file| { + Ok(match file_type { + StorageMethod::JSON => serde_json::from_reader(file)?, + StorageMethod::CBOR => cbor4ii::serde::from_reader(BufReader::new(file)) + .map_err(io::Error::other)?, + }) + })?; + Self::make_weak_config(&policyfile.data.data); + policyfile + .data + .upgrade_version(POLICY_MIGRATIONS) + .map_err(|e| io::Error::other(e.to_string()))?; + debug!("{}", serde_json::to_string_pretty(&policyfile.data.data)?); + Ok(policyfile) + } + + fn make_weak_config(config: &Rc>) { + for role in &config.as_ref().borrow().roles { + role.as_ref().borrow_mut().config = Some(Rc::downgrade(config)); + for task in &role.as_ref().borrow().tasks { + task.as_ref().borrow_mut().role = Some(Rc::downgrade(role)); + } + } + } + + /// # Errors + /// Returns an error if any of the files cannot be written + pub fn save_all(&mut self) -> Result<(), Box> { + let immutable = self + .root + .data + .data + .storage + .settings + .as_ref() + .and_then(|s| s.immutable) + .unwrap_or(RAR_CFG_IMMUTABLE); + + if let Some(path) = self.root.data.data.storage.settings.as_ref().and_then(|s| { + s.path.as_ref().and_then(|p| { + debug!( + "Checking if root settings path needs to be updated: current {}, new {}", + p.display(), + p.display() + ); + if *p == self.root.path { + None + } else { + Some(p.clone()) + } + }) + }) { + // Open the new file with the new path + let new_root = LockedSettingsFile::open_write(path, |_, _| { + Ok(Versioning::new(self.root.data.data.clone())) + })?; + debug!( + "Moved root settings to new path: {}", + new_root.path.display() + ); + // Manually drop the old root using unsafe code to trigger Drop impl + unsafe { + let old_root_ptr = &raw mut self.root; + std::ptr::drop_in_place(old_root_ptr); + } + self.root = new_root; + } + + let mut has_errors = if let Err(e) = self.root.save(RAR_CFG_TYPE, immutable) { + error!("Failed to save root settings: {e}"); + true + } else { + debug!("Saved root settings"); + false + }; + + for config in &mut self.map { + if let Err(e) = config.save(self.root.data.data.storage.method, immutable) { + error!( + "Failed to save settings for {}: {}", + config.path.display(), + e + ); + has_errors = true; + } + } + + if has_errors { + Err("One or more files failed to save. Check the logs for details.".into()) + } else { + Ok(()) + } + } + + #[must_use] + pub fn get_files(&self) -> Vec> { + let mut vec: Vec<_> = self.map.iter().map(|e| e.path.to_string_lossy()).collect(); + vec.push(RAR_CFG_PATH.into()); + vec + } + + #[must_use] + pub fn get(&self, path: &Path) -> Option<&Rc>> { + if path == RAR_CFG_PATH { + self.root.data.data.config.as_ref() + } else { + self.map + .iter() + .find(|config| config.path == path) + .map(|config| &config.data.data) + } + } + + #[must_use] + pub const fn get_root(&self) -> &RootSettings { + &self.root.data.data + } + + pub const fn get_root_mut(&mut self) -> &mut RootSettings { + &mut self.root.data.data + } + + #[must_use] + pub fn get_policies(&self) -> Vec<&Rc>> { + self.map.iter().map(|config| &config.data.data).collect() + } +} + +#[cfg(test)] +mod tests { + use std::io::{Read, Write}; + + use crate::database::actor::SActor; + use crate::database::structs::{SCommand, SCommands, SCredentials, SRole, STask, SetBehavior}; + use crate::{PACKAGE_VERSION, RemoteStorageSettings}; + + use super::*; + + pub struct Defer(Option); + + impl Defer { + pub fn new(f: F) -> Self { + Self(Some(f)) + } + } + + impl Drop for Defer { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f(); + } + } + } + + pub fn defer(f: F) -> Defer { + Defer::new(f) + } + + #[test] + fn test_get_settings_same_file() { + // Create a test JSON file + let value = "/tmp/test_get_settings_same_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(value); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let settings = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(value) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SPolicy::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple( + "/usr/bin/true".to_string(), + )]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + let mut config = + LockedSettingsFile::open_write(PathBuf::from(value), |_, _| Ok(settings.clone())) + .unwrap(); + config.save(StorageMethod::JSON, false).unwrap(); + + let full = FileSettings::read_all(value, value, StorageMethod::JSON).unwrap(); + assert_eq!(*full.get_root(), settings); + } + + #[test] + fn test_get_settings_different_file() { + // Create a test JSON file + let external_file_path = "/tmp/test_get_settings_different_file_external.json"; + let test_file_path = "/tmp/test_get_settings_different_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file_path) + .canonicalize() + .unwrap_or_else(|_| test_file_path.into()); + if std::fs::remove_file(test_file_path).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file_path) + .canonicalize() + .unwrap_or_else(|_| external_file_path.into()); + if std::fs::remove_file(external_file_path).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let settings_config = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(external_file_path) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SPolicy::builder() + .role(SRole::builder("IGNORED").build()) + .build(), + ) + .build(); + let mut file = + LockedSettingsFile::open_write(test_file_path, |_, _| Ok(settings_config.clone())) + .unwrap(); + file.save(StorageMethod::JSON, false).unwrap(); + let config = SPolicy::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + let mut file = + LockedSettingsFile::open_write(external_file_path, |_, _| Ok(config.clone())).unwrap(); + file.save(StorageMethod::JSON, false).unwrap(); + + let full = FileSettings::read_all(test_file_path, external_file_path, StorageMethod::JSON) + .unwrap(); + assert_eq!(full.get_policies().len(), 1); + assert_eq!(*full.get_policies()[0].borrow(), *file.data.borrow()); + } + + #[test] + fn test_save_settings_same_file() { + let test_file = "/tmp/test_save_settings_same_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + let settings = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(test_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SPolicy::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple( + "/usr/bin/true".to_string(), + )]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + let mut config = + LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| Ok(settings.clone())) + .unwrap(); + config.save(StorageMethod::JSON, false).unwrap(); + } + + #[test] + fn test_save_settings_different_file() { + let external_file = "/tmp/test_save_settings_different_file_external.json"; + let test_file = "/tmp/test_save_settings_different_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file) + .canonicalize() + .unwrap_or_else(|_| external_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + let settings_config = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(external_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SPolicy::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple( + "/usr/bin/true".to_string(), + )]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + let mut config = LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| { + Ok(settings_config.clone()) + }) + .unwrap(); + config.save(StorageMethod::JSON, false).unwrap(); + + // assert that external_file contains /usr/bin/true + let mut file = read_with_privileges(external_file).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + assert!(content.contains("/usr/bin/true")); + + // assert that test_file does NOT contain /usr/bin/true (only storage settings) + let mut file = read_with_privileges(test_file).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + assert!(!content.contains("/usr/bin/true")); + } + + #[test] + fn test_save_cbor_format() { + let external_file = "/tmp/test_save_cbor_format.bin"; + let test_file = "/tmp/test_save_cbor_format.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file) + .canonicalize() + .unwrap_or_else(|_| external_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + let settings = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::CBOR) + .settings( + RemoteStorageSettings::builder() + .path(external_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SPolicy::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple( + "/usr/bin/true".to_string(), + )]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + let mut config = + LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| Ok(settings.clone())) + .unwrap(); + config.save(StorageMethod::CBOR, false).unwrap(); + + // Assert that external_file is a binary file with CBOR format + let mut file = read_with_privileges(external_file).unwrap(); + let mut content = Vec::new(); + file.read_to_end(&mut content).unwrap(); + let deserialized: Versioning>> = + cbor4ii::serde::from_reader(&content[..]).unwrap(); + assert_eq!(deserialized.version, PACKAGE_VERSION); + } + + #[test] + fn test_locked_settings_file_open_new_file() { + let test_file = "/tmp/test_locked_settings_file_open_new_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + // Test opening a non-existent file with write mode + let locked_file = LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| { + Ok(RootSettings::default()) + }) + .unwrap(); + + // Should create default settings + assert_eq!(locked_file.path, PathBuf::from(test_file)); + assert_eq!(locked_file.data, RootSettings::default()); + } + + #[test] + fn test_locked_settings_file_open_existing_file() { + let test_file = "/tmp/test_locked_settings_file_open_existing_file.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + // Create and save a test file with some content + let settings = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(test_file) + .not_immutable() + .build(), + ) + .build(), + ) + .config( + SPolicy::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple( + "/usr/bin/true".to_string(), + )]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + + let mut config = + LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| Ok(settings.clone())) + .unwrap(); + config.save(StorageMethod::JSON, false).unwrap(); + + // Test opening existing file + let locked_file = LockedSettingsFile::open_read(PathBuf::from(test_file), |_, file| { + let versioned: Versioning = serde_json::from_reader(file)?; + Ok(versioned.data) + }) + .unwrap(); + + // Should load the existing settings + assert_eq!(locked_file.path, PathBuf::from(test_file)); + assert_eq!(locked_file.data, settings); + } + + #[test] + fn test_locked_settings_file_open_write_mode_non_immutable() { + let test_file = "/tmp/test_locked_settings_file_open_write_mode_non_immutable.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + // Create a test file with non-immutable settings + let settings = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(test_file) + .not_immutable() // explicitly not immutable + .build(), + ) + .build(), + ) + .build(); + + let mut config = + LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| Ok(settings.clone())) + .unwrap(); + config.save(StorageMethod::JSON, false).unwrap(); + + // Test opening existing file with write mode - should work normally for non-immutable files + let result = LockedSettingsFile::open_write(PathBuf::from(test_file), |_, file| { + let versioned: Versioning = serde_json::from_reader(file)?; + Ok(versioned.data) + }); + match result { + Ok(locked_file) => { + assert_eq!(locked_file.path, PathBuf::from(test_file)); + // The loaded settings should match our created config + assert_eq!(locked_file.data.storage, settings.storage); + } + Err(_) => { + println!("Test skipped due to insufficient privileges in test environment"); + } + } + } + + #[test] + fn test_locked_settings_file_open_with_separate_config() { + let test_file = "/tmp/test_locked_settings_file_open_with_separate_config.json"; + let external_file = + "/tmp/test_locked_settings_file_open_with_separate_config_external.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + let _cleanup2 = defer(|| { + let filename = PathBuf::from(external_file) + .canonicalize() + .unwrap_or_else(|_| external_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + // Create external config file + let sconfig = SPolicy::builder() + .role( + SRole::builder("test_role") + .actor(SActor::user(0).build()) + .task( + STask::builder("test_task") + .cred(SCredentials::builder().setuid(0).setgid(0).build()) + .commands( + SCommands::builder(SetBehavior::None) + .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) + .build(), + ) + .build(), + ) + .build(), + ) + .build(); + let mut external_config = LockedSettingsFile::open_write( + PathBuf::from(external_file), + |_, _| Ok(sconfig.clone()), + ) + .unwrap(); + external_config.save(StorageMethod::JSON, false).unwrap(); + drop(external_config); + + // Create settings file pointing to external config + let settings_config = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(external_file) + .not_immutable() + .build(), + ) + .build(), + ) + .build(); + let mut config = LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| { + Ok(settings_config.clone()) + }) + .unwrap(); + config.save(StorageMethod::JSON, false).unwrap(); + + // Test opening file with separate config + let locked_file = LockedSettingsFile::open_read(PathBuf::from(test_file), |_, file| { + let versioned: Versioning = serde_json::from_reader(file)?; + Ok(versioned.data) + }) + .unwrap(); + + // Should load settings and external config + assert_eq!(locked_file.path, PathBuf::from(test_file)); + assert_eq!(locked_file.data.storage, settings_config.storage); + } + + #[test] + fn test_locked_settings_file_open_invalid_json() { + let test_file = "/tmp/test_locked_settings_file_open_invalid_json.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + // Create a file with invalid JSON + let mut file = File::create(test_file).unwrap(); + file.write_all(b"{ invalid json content }").unwrap(); + drop(file); + + // Test opening file with invalid JSON - should fall back to default + let locked_file = LockedSettingsFile::open_read(PathBuf::from(test_file), |_, file| { + match serde_json::from_reader::<_, Versioning>(file) { + Ok(versioned) => Ok(versioned.data), + Err(_) => Ok(RootSettings::default()), + } + }) + .unwrap(); + + // Should fall back to default settings when JSON is invalid + assert_eq!(locked_file.path, PathBuf::from(test_file)); + assert_eq!(locked_file.data, RootSettings::default()); + } + + #[test] + fn test_locked_settings_file_open_readonly() { + let test_file = "/tmp/test_locked_settings_file_open_readonly.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + // Create a test file with minimal settings + let settings = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .build(), + ) + .build(); + + let mut config = + LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| Ok(settings.clone())) + .unwrap(); + config.save(StorageMethod::JSON, false).unwrap(); + + // Test opening file in read-only mode + let locked_file = LockedSettingsFile::open_read(PathBuf::from(test_file), |_, file| { + let versioned: Versioning = serde_json::from_reader(file)?; + Ok(versioned.data) + }) + .unwrap(); + + // Should successfully open and load settings + assert_eq!(locked_file.path, PathBuf::from(test_file)); + // The storage settings should match what we wrote + assert_eq!(locked_file.data.storage.method, settings.storage.method); + assert_eq!(locked_file.data.storage.settings, settings.storage.settings); + } + + #[test] + fn test_locked_settings_file_open_nonexistent_file_error() { + let test_file = "/tmp/test_locked_settings_file_open_nonexistent_file_error.json"; + + // Ensure the file doesn't exist + let _ = std::fs::remove_file(test_file); + + // Test opening non-existent file without create option - should fail + let result = LockedSettingsFile::open_read(PathBuf::from(test_file), |_, file| { + let versioned: Versioning = serde_json::from_reader(file)?; + Ok(versioned.data) + }); + + // Should fail because file doesn't exist + assert!(result.is_err()); + } + + #[test] + fn test_locked_settings_file_open_create_new() { + let test_file = "/tmp/test_locked_settings_file_open_create_new.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + // Ensure the file doesn't exist + let _ = std::fs::remove_file(test_file); + + // Test creating a new file + let locked_file = LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| { + Ok(RootSettings::default()) + }) + .unwrap(); + + // Should create new file with default settings + assert_eq!(locked_file.path, PathBuf::from(test_file)); + // File should exist now + assert!(PathBuf::from(test_file).exists()); + } + + #[test] + fn test_locked_settings_truncates_file_on_save() { + let test_file = "/tmp/test_locked_settings_truncates_file_on_save.json"; + let _cleanup = defer(|| { + let filename = PathBuf::from(test_file) + .canonicalize() + .unwrap_or_else(|_| test_file.into()); + if std::fs::remove_file(&filename).is_err() { + debug!("Failed to delete the file: {}", filename.display()); + } + }); + + // Create a test file with some initial content + let initial_content = r#"{ + "version": "0.1.0", + "storage": { + "method": "JSON" + } + }"#; + let mut file = File::create(test_file).unwrap(); + file.write_all(initial_content.as_bytes()).unwrap(); + drop(file); + + // Create new settings with no config + let settings = RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .build(), + ) + .build(); + + // Open and save - should truncate old content + let mut locked = + LockedSettingsFile::open_write(PathBuf::from(test_file), |_, _| Ok(settings.clone())) + .unwrap(); + locked.save(StorageMethod::JSON, false).unwrap(); + + // Read back the file content + let mut file = File::open(test_file).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + + // The content should NOT contain old roles + assert!(!content.contains("old_role")); + assert!(!content.contains("another_old_role")); + assert!(!content.contains("yet_another_old_role")); + assert!(!content.contains("oldest_role")); + } +} diff --git a/rar-common/src/ldap.rs b/rar-common/src/ldap.rs new file mode 100644 index 00000000..d7e49fec --- /dev/null +++ b/rar-common/src/ldap.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +use crate::ConnectionAuth; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct LdapSettings { + pub enabled: bool, + pub host: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub auth: Option, + /// The base DN for LDAP searches, e.g., "dc=example,dc=com" + pub base_dn: String, + /// The DN where user entries are located, e.g., "ou=users" + pub user_dn: String, + /// The DN where group entries are located, e.g., "ou=groups" + pub group_dn: String, + /// The name of the attribute used to filter user entries, e.g., "uid" + pub user_filter: String, + /// The name of the attribute used to filter group entries, e.g., "gid" + pub group_filter: String, + /// The name of the attribute used to retrieve roles for a user, e.g., "memberOf" + pub role_filter: String, +} +// diff --git a/rar-common/src/lib.rs b/rar-common/src/lib.rs index 2e235285..36396b71 100644 --- a/rar-common/src/lib.rs +++ b/rar-common/src/lib.rs @@ -48,113 +48,51 @@ // } pub const PACKAGE_VERSION: semver::Version = semver::Version::new( - konst::unwrap_ctx!(konst::primitive::parse_u64(env!("CARGO_PKG_VERSION_MAJOR"))), - konst::unwrap_ctx!(konst::primitive::parse_u64(env!("CARGO_PKG_VERSION_MINOR"))), - konst::unwrap_ctx!(konst::primitive::parse_u64(env!("CARGO_PKG_VERSION_PATCH"))), + result::unwrap!(u64::from_str_radix(env!("CARGO_PKG_VERSION_MAJOR"), 10)), + result::unwrap!(u64::from_str_radix(env!("CARGO_PKG_VERSION_MINOR"), 10)), + result::unwrap!(u64::from_str_radix(env!("CARGO_PKG_VERSION_PATCH"), 10)), ); -use std::{ - cell::RefCell, - error::Error, - fs::{File, Permissions}, - io::{BufReader, Seek}, - ops::DerefMut, - os::unix::fs::{MetadataExt, PermissionsExt}, - path::{Path, PathBuf}, - rc::Rc, -}; +use std::{io::Error, path::PathBuf}; -use bon::{Builder, builder}; -use capctl::Cap; +use bon::Builder; +use konst::result; use libc::dev_t; -use log::{debug, warn}; -use nix::{ - fcntl::Flock, - unistd::{Gid, Group, Pid, Uid, User, getgroups}, -}; -use serde::{Deserialize, Serialize, ser::SerializeMap}; +use nix::unistd::{Gid, Group, Pid, Uid, User, getgroups}; +use serde::{Deserialize, Serialize}; //pub mod api; pub mod database; //pub mod plugin; +pub mod file; +#[cfg(feature = "ldap")] +pub mod ldap; pub mod util; -use strum::EnumString; -use util::{read_with_privileges, write_cbor_config, write_json_config}; - -use database::{ - migration::Migration, - structs::SConfig, - versionning::{SETTINGS_MIGRATIONS, Versioning}, -}; - -use crate::util::{ - has_privileges, is_immutable, open_lock_with_privileges, with_mutable_config, with_privileges, -}; +#[cfg(feature = "ldap")] +use crate::ldap::LdapSettings; +use crate::util::{Either, RAR_CFG_TYPE, StorageMethod}; #[derive(Debug, Builder)] +#[allow(clippy::missing_errors_doc)] pub struct Cred { - #[builder(field = User::from_uid(Uid::current()).unwrap().unwrap())] + #[builder(with = || -> Result<_,Error> { + let uid = Uid::current(); + User::from_uid(uid)?.ok_or_else(|| Error::other("User not found"))}) + ] pub user: User, - #[builder(field = getgroups().unwrap().iter().map(|gid| Group::from_gid(*gid).unwrap().unwrap()) - .collect())] - pub groups: Vec, + #[builder(with = || -> Result<_,Error> { + Ok(getgroups()? + .iter() + .map(|gid| Either::from(Group::from_gid(*gid).ok().flatten().ok_or(*gid))) + .collect()) + })] + pub groups: Vec>, pub tty: Option, #[builder(default = nix::unistd::getppid(), into)] pub ppid: Pid, -} - -#[derive( - Serialize, - Deserialize, - Debug, - Clone, - PartialEq, - Eq, - Default, - Copy, - EnumString, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -#[repr(u8)] -pub enum StorageMethod { - #[default] - #[strum(ascii_case_insensitive)] - JSON, - #[strum(ascii_case_insensitive)] - CBOR, - // SQLite, - // PostgreSQL, - // MySQL, - // LDAP, -} - -pub struct LockedSettingsFile { - path: PathBuf, - fd: Flock, // file descriptor to the opened file, to keep the lock - pub data: Rc>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq, Default)] -pub struct Settings { - pub storage: SettingsContent, -} - -#[derive(Debug, Clone, Builder, PartialEq, Eq, Default)] -pub struct FullSettings { - pub storage: SettingsContent, - pub config: Option>>, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq)] -pub struct SettingsContent { - #[builder(default = StorageMethod::JSON, into)] - pub method: StorageMethod, - #[serde(skip_serializing_if = "Option::is_none")] - pub settings: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ldap: Option, + #[builder(with = || -> Result<_, std::io::Error> { std::env::current_dir() })] + pub curdir: PathBuf, } #[derive(Serialize, Deserialize, Debug, Clone, Builder, Default, PartialEq, Eq)] @@ -165,22 +103,47 @@ pub struct RemoteStorageSettings { #[serde(skip_serializing_if = "Option::is_none")] #[builder(into)] pub path: Option, + #[cfg(feature = "sgbd")] #[serde(skip_serializing_if = "Option::is_none")] pub host: Option, + #[cfg(feature = "sgbd")] #[serde(skip_serializing_if = "Option::is_none")] pub port: Option, + #[cfg(feature = "sgbd")] #[serde(skip_serializing_if = "Option::is_none")] pub auth: Option, + #[cfg(feature = "sgbd")] #[serde(skip_serializing_if = "Option::is_none")] pub database: Option, + #[cfg(feature = "sgbd")] #[serde(skip_serializing_if = "Option::is_none")] pub schema: Option, + #[cfg(feature = "sgbd")] #[serde(skip_serializing_if = "Option::is_none")] pub table_prefix: Option, + #[cfg(feature = "sgbd")] #[serde(skip_serializing_if = "Option::is_none")] pub properties: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct Properties { + pub use_unicode: bool, + pub character_encoding: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq, Default)] +pub struct SettingsContent { + #[builder(default = RAR_CFG_TYPE, into)] + pub method: StorageMethod, + #[serde(skip_serializing_if = "Option::is_none")] + pub settings: Option, + + #[cfg(feature = "ldap")] + #[serde(skip_serializing_if = "Option::is_none")] + pub ldap: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ConnectionAuth { pub user: String, @@ -201,1251 +164,8 @@ pub struct ClientSsl { pub client_key: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct Properties { - pub use_unicode: bool, - pub character_encoding: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct LdapSettings { - pub enabled: bool, - pub host: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub port: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub auth: Option, - pub base_dn: String, - pub user_dn: String, - pub group_dn: String, - pub user_filter: String, - pub group_filter: String, -} - -impl Serialize for FullSettings { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut map = serializer.serialize_map(None)?; - map.serialize_entry("storage", &self.storage)?; - // Flatten config fields into the main object - if let Some(config) = &self.config { - let config_value = - serde_json::to_value(&*config.borrow()).map_err(serde::ser::Error::custom)?; - if let serde_json::Value::Object(obj) = config_value { - for (key, value) in obj { - map.serialize_entry(&key, &value)?; - } - } - } - map.end() - } -} - -impl<'de> Deserialize<'de> for FullSettings { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct FullSettingsVisitor; - - impl<'de> serde::de::Visitor<'de> for FullSettingsVisitor { - type Value = FullSettings; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("struct FullSettings") - } - - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - let mut storage = None; - let mut config_fields = std::collections::HashMap::new(); - - while let Some(key) = map.next_key::()? { - match key.as_str() { - "storage" | "s" => { - if storage.is_some() { - return Err(serde::de::Error::duplicate_field("storage")); - } - storage = Some(map.next_value()?); - } - // Collect all other fields as potential config fields - _ => { - config_fields.insert(key, map.next_value::()?); - } - } - } - - let storage = storage.ok_or_else(|| serde::de::Error::missing_field("storage"))?; - - // If we have config fields, deserialize them into SConfig - let config = if config_fields.is_empty() { - None - } else { - let config_value = - serde_json::Value::Object(config_fields.into_iter().collect()); - Some(Rc::new(RefCell::new( - SConfig::deserialize(config_value).map_err(serde::de::Error::custom)?, - ))) - }; - - Ok(FullSettings { storage, config }) - } - } - - deserializer.deserialize_map(FullSettingsVisitor) - } -} - -// Default implementation for Settings -impl Default for SettingsContent { - fn default() -> Self { - Self { - method: StorageMethod::JSON, - settings: None, - ldap: None, - } - } -} - -pub fn make_weak_config(config: &Rc>) { - for role in &config.as_ref().borrow().roles { - role.as_ref().borrow_mut().config = Some(Rc::downgrade(config)); - for task in &role.as_ref().borrow().tasks { - task.as_ref().borrow_mut().role = Some(Rc::downgrade(role)); - } - } -} - -/// This opens, deserialize and locks a settings file, and keeps the file descriptor open to keep the lock -/// it allows to save the settings file later -impl LockedSettingsFile { - /// # Errors - /// Returns an error if the file cannot be opened, deserialized or locked - pub fn open(path: S, options: &std::fs::OpenOptions, write: bool) -> std::io::Result - where - S: AsRef, - { - if write && path.as_ref().exists() { - let mut file = read_with_privileges(&path)?; - if is_immutable(&file)? { - return with_mutable_config(&mut file, |_| { - let file = open_lock_with_privileges( - path.as_ref(), - options, - nix::fcntl::FlockArg::LockExclusive, - )?; - - Ok(Self { - path: path.as_ref().to_path_buf(), - data: load_full_settings(&path, &file) - .unwrap_or_else(|_| Rc::new(RefCell::new(FullSettings::default()))), - fd: file, - }) - }); - } - } - - let file = - open_lock_with_privileges(path.as_ref(), options, nix::fcntl::FlockArg::LockExclusive)?; - - Ok(Self { - path: path.as_ref().to_path_buf(), - data: load_full_settings(&path, &file) - .unwrap_or_else(|_| Rc::new(RefCell::new(FullSettings::default()))), - fd: file, - }) - } +#[cfg(feature = "sgbd")] +compile_error!("SGBD feature is not yet implemented"); - /// # Errors - /// Returns an error if the file cannot be written - /// due to a lock, permission error or writing error - pub fn save(&mut self) -> Result<(), Box> { - debug!("Saving settings file: {}", self.path.display()); - Migration::migrate( - &PACKAGE_VERSION, - &mut *self.data.as_ref().borrow_mut(), - SETTINGS_MIGRATIONS, - )?; - debug!("Migrated settings to version {PACKAGE_VERSION}"); - let immuable = self - .data - .as_ref() - .borrow() - .storage - .settings - .clone() - .unwrap_or_default() - .immutable - .unwrap_or(env!("RAR_CFG_IMMUTABLE") == "true") - && has_privileges(&[Cap::LINUX_IMMUTABLE])?; - debug!("Settings file immutable: {immuable}"); - let separate = if let Some(rss) = &self.data.as_ref().borrow().storage.settings { - let default_data_path = env!("RAR_CFG_DATA_PATH").to_string().into(); - let data_path = rss.path.as_ref().unwrap_or(&default_data_path); - if *data_path == self.path { - None - } else { - Some(data_path.clone()) - } - } else { - None - }; - debug!("Settings file separate: {separate:?}"); - if let Some(data_path) = separate { - debug!("Saving settings in separate file"); - return self.separate_save(&data_path, immuable); - } - let versionned: Versioning>> = Versioning::new(self.data.clone()); - if immuable { - debug!("Toggling immutable off for config file"); - with_mutable_config(&mut self.fd, |file| { - debug!("Toggled immutable off for config file"); - file.rewind()?; - file.set_len(0)?; - write_json_config(&versionned, file) - })?; - } else { - let file = &mut *self.fd; - debug!("Writing config file"); - file.rewind()?; - debug!("Rewound config file for writing"); - file.set_len(0)?; - debug!("Truncated config file"); - write_json_config(&versionned, file)?; - // clear the rest of the file if any - debug!("Wrote config file"); - } - Ok(()) - } - - /// # Errors - /// Returns an error if the separate file cannot be written - /// due to a lock, permission error or writing error - fn separate_save(&mut self, data_path: &T, immutable: bool) -> Result<(), Box> - where - T: AsRef, - { - { - let storage_method = self.data.as_ref().borrow().storage.method; - let binding = self.data.as_ref().borrow_mut(); - let config = binding.config.as_ref().unwrap(); - let versioned_config: Versioning>> = - Versioning::new(config.clone()); - let mut file = open_lock_with_privileges( - data_path.as_ref(), - &std::fs::OpenOptions::new() - .truncate(true) - .write(true) - .create(true) - .to_owned(), - nix::fcntl::FlockArg::LockExclusive, - )?; - if immutable { - with_mutable_config(&mut file, |file| { - write_storage_settings() - .path(data_path.as_ref()) - .fd(file) - .method(storage_method) - .config(&versioned_config) - .set_read_only(!cfg!(test)) - .set_root_owner(!cfg!(test)) - .call() - })?; - } else { - write_storage_settings() - .path(data_path.as_ref()) - .fd(&mut file) - .method(storage_method) - .config(&versioned_config) - .set_read_only(!cfg!(test)) - .set_root_owner(!cfg!(test)) - .call()?; - } - } - self.data.as_ref().borrow_mut().config = None; - let versioned_settings: Versioning>> = - Versioning::new(self.data.clone()); - self.fd.deref_mut().rewind()?; - if immutable { - debug!("Toggling immutable off for config file"); - with_mutable_config(&mut self.fd, |file| { - write_json_config(&versioned_settings, file) - })?; - } else { - write_json_config(&versioned_settings, &mut *self.fd)?; - } - Ok(()) - } -} - -#[builder] -fn write_storage_settings

( - path: P, - fd: &mut File, - method: StorageMethod, - config: &Versioning>>, - #[builder(default = false)] set_read_only: bool, - #[builder(default = false)] set_root_owner: bool, -) -> std::io::Result<()> -where - P: AsRef, -{ - debug!( - "Saving in {} : {}", - path.as_ref().display(), - serde_json::to_string_pretty(&config).unwrap() - ); - match method { - StorageMethod::JSON => write_json_config(config, fd), - StorageMethod::CBOR => write_cbor_config(config, fd), - }?; - if set_read_only { - if Uid::current().as_raw() == path.as_ref().metadata()?.uid() { - let perms = Permissions::from_mode(0o400); - std::fs::set_permissions(path.as_ref(), perms)?; - } else { - with_privileges(&[Cap::FOWNER], || { - let perms = Permissions::from_mode(0o400); - std::fs::set_permissions(path.as_ref(), perms) - })?; - } - } - if set_root_owner { - with_privileges(&[Cap::CHOWN], || { - nix::unistd::chown( - path.as_ref(), - Some(Uid::from_raw(0)), - Some(Gid::from_raw(0)), - ) - .map_err(|e| std::io::Error::from_raw_os_error(e as i32)) - })?; - } - Ok(()) -} - -/// # Errors -/// Returns an error if the file cannot be opened or deserialized -pub fn read_full_settings(path: &S) -> Result>, Box> -where - S: AsRef, -{ - // if user does not have read permission, try to enable privilege - let file = read_with_privileges(path.as_ref())?; - load_full_settings(path, &file) -} - -/// # Errors -/// Returns an error if the file cannot be opened or deserialized -fn load_full_settings>( - path: &S, - file: &File, -) -> Result>, Box> { - let value: Versioning = serde_json::from_reader(file).inspect_err(|e| { - debug!("Error reading file: {e}"); - })?; - let settingsfile = rc_refcell!(value.data); - debug!("settingsfile: {settingsfile:?}"); - let default_remote = RemoteStorageSettings::default(); - let into = env!("RAR_CFG_DATA_PATH").to_string().into(); - { - let mut binding = settingsfile.as_ref().borrow_mut(); - let data_path = binding - .storage - .settings - .as_ref() - .unwrap_or(&default_remote) - .path - .as_ref() - .unwrap_or(&into); - if data_path != path.as_ref() { - binding.config = Some(retrieve_sconfig(&binding.storage.method, data_path)?); - } else if let Some(config) = &binding.config { - make_weak_config(config); - } - } - Ok(settingsfile) -} - -/// # Errors -/// Returns an error if the file cannot be opened or deserialized -pub fn retrieve_sconfig( - file_type: &StorageMethod, - path: &PathBuf, -) -> Result>, Box> { - let file = read_with_privileges(path)?; - let value: Versioning>> = match file_type { - StorageMethod::JSON => serde_json::from_reader(file)?, - StorageMethod::CBOR => cbor4ii::serde::from_reader(BufReader::new(file))?, - }; - make_weak_config(&value.data); - //read_effective(false).or(dac_override_effective(false))?; - //assert_eq!(value.version.to_string(), PACKAGE_VERSION, "Version mismatch"); - debug!("{}", serde_json::to_string_pretty(&value)?); - Ok(value.data) -} - -/// # Errors -/// Returns an error if the migration fails -pub fn migrate_settings(settings: &mut FullSettings) -> Result<(), Box> { - Migration::migrate(&PACKAGE_VERSION, settings, SETTINGS_MIGRATIONS)?; - Ok(()) -} - -/// # Errors -/// Returns an error if the file cannot be opened -pub fn get_settings(path: &S) -> Result> -where - S: AsRef, -{ - // if user does not have read permission, try to enable privilege - let file = read_with_privileges(path.as_ref())?; - let value: Versioning = serde_json::from_reader(file) - .inspect_err(|e| { - debug!("Error reading file: {e}"); - }) - .unwrap_or_else(|_| { - warn!("Using default settings file!!"); - Versioning::default() - }); - //read_effective(false).or(dac_override_effective(false))?; - debug!("{}", serde_json::to_string_pretty(&value)?); - Ok(value.data) -} - -#[cfg(test)] -mod tests { - use std::fs; - use std::io::{Read, Write}; - - use crate::database::actor::SActor; - use crate::database::structs::{SCommand, SCommands, SCredentials, SRole, STask, SetBehavior}; - use crate::util::unlock_immutable; - - use super::*; - - pub struct Defer(Option); - - impl Defer { - pub fn new(f: F) -> Self { - Self(Some(f)) - } - } - - impl Drop for Defer { - fn drop(&mut self) { - if let Some(f) = self.0.take() { - f(); - } - } - } - - pub fn defer(f: F) -> Defer { - Defer::new(f) - } - - #[test] - fn test_get_settings_same_file() { - // Create a test JSON file - let value = "/tmp/test_get_settings_same_file.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(value) - .canonicalize() - .unwrap_or_else(|_| value.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - let mut file = File::create(value).unwrap(); - let config = Versioning::new(Rc::new(RefCell::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .settings( - RemoteStorageSettings::builder() - .path(value) - .not_immutable() - .build(), - ) - .build(), - ) - .config( - SConfig::builder() - .role( - SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task( - STask::builder("test_task") - .cred(SCredentials::builder().setuid(0).setgid(0).build()) - .commands( - SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple( - "/usr/bin/true".to_string(), - )]) - .build(), - ) - .build(), - ) - .build(), - ) - .build(), - ) - .build(), - ))); - write_json_config(&config, &mut file).unwrap(); - let settings = read_full_settings(&value).unwrap(); - assert_eq!(*config.data.borrow(), *settings.as_ref().borrow()); - fs::remove_file(value).unwrap(); - } - - #[test] - fn test_get_settings_different_file() { - // Create a test JSON file - let external_file_path = "/tmp/test_get_settings_different_file_external.json"; - let test_file_path = "/tmp/test_get_settings_different_file.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file_path) - .canonicalize() - .unwrap_or_else(|_| test_file_path.into()); - if std::fs::remove_file(test_file_path).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - let _cleanup2 = defer(|| { - let filename = PathBuf::from(external_file_path) - .canonicalize() - .unwrap_or_else(|_| external_file_path.into()); - if std::fs::remove_file(external_file_path).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - let mut external_file = File::create(external_file_path).unwrap(); - let mut test_file = File::create(test_file_path).unwrap(); - let settings_config = Versioning::new(Rc::new(RefCell::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .settings( - RemoteStorageSettings::builder() - .path(external_file_path) - .not_immutable() - .build(), - ) - .build(), - ) - .config( - SConfig::builder() - .role(SRole::builder("IGNORED").build()) - .build(), - ) - .build(), - ))); - write_json_config(&settings_config, &mut test_file).unwrap(); - let config = SConfig::builder() - .role( - SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task( - STask::builder("test_task") - .cred(SCredentials::builder().setuid(0).setgid(0).build()) - .commands( - SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) - .build(), - ) - .build(), - ) - .build(), - ) - .build(); - write_json_config(&Versioning::new(config.clone()), &mut external_file).unwrap(); - let settings = read_full_settings(&test_file_path).unwrap(); - assert_eq!( - *config.borrow(), - *settings.as_ref().borrow().config.as_ref().unwrap().borrow() - ); - fs::remove_file(test_file_path).unwrap(); - fs::remove_file(external_file_path).unwrap(); - } - - #[test] - fn test_save_settings_same_file() { - let test_file = "/tmp/test_save_settings_same_file.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - // Create a test JSON file - let config = Rc::new(RefCell::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .settings( - RemoteStorageSettings::builder() - .path(test_file) - .not_immutable() - .build(), - ) - .build(), - ) - .config( - SConfig::builder() - .role( - SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task( - STask::builder("test_task") - .cred(SCredentials::builder().setuid(0).setgid(0).build()) - .commands( - SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple( - "/usr/bin/true".to_string(), - )]) - .build(), - ) - .build(), - ) - .build(), - ) - .build(), - ) - .build(), - )); - let file = File::create(test_file).unwrap(); - let file = Flock::lock(file, nix::fcntl::FlockArg::LockExclusive).unwrap(); - let mut settingsfile = LockedSettingsFile { - path: PathBuf::from(test_file), - fd: file, - data: config.clone(), - }; - settingsfile.save().unwrap(); - let settings = read_full_settings(&test_file).unwrap(); - assert_eq!(*config.borrow(), *settings.borrow()); - fs::remove_file(test_file).unwrap(); - } - - #[test] - fn test_save_settings_different_file() { - let external_file = "/tmp/test_save_settings_different_file_external.json"; - let test_file = "/tmp/test_save_settings_different_file.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - let _cleanup2 = defer(|| { - let filename = PathBuf::from(external_file) - .canonicalize() - .unwrap_or_else(|_| external_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - let sconfig = SConfig::builder() - .role( - SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task( - STask::builder("test_task") - .cred(SCredentials::builder().setuid(0).setgid(0).build()) - .commands( - SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) - .build(), - ) - .build(), - ) - .build(), - ) - .build(); - // Create a test JSON file - let config = Rc::new(RefCell::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .settings( - RemoteStorageSettings::builder() - .path(external_file) - .not_immutable() - .build(), - ) - .build(), - ) - .config(sconfig.clone()) - .build(), - )); - let file = File::create(test_file).unwrap(); - let file = Flock::lock(file, nix::fcntl::FlockArg::LockExclusive).unwrap(); - let mut settingsfile = LockedSettingsFile { - path: PathBuf::from(test_file), - fd: file, - data: config.clone(), - }; - settingsfile.save().unwrap(); - //assert that test_external.json contains /usr/bin/true - let mut file = read_with_privileges(external_file).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - assert!(content.contains("/usr/bin/true")); - - let mut file = read_with_privileges(test_file).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - assert!(!content.contains("/usr/bin/true")); - - let settings = read_full_settings(&test_file).unwrap(); - assert_eq!( - *sconfig.borrow(), - *settings.borrow().config.as_ref().unwrap().borrow() - ); - settings.as_ref().borrow_mut().config = None; - assert_eq!(*config.borrow(), *settings.borrow()); - fs::remove_file(test_file).unwrap(); - fs::remove_file(external_file).unwrap(); - } - - #[test] - fn test_save_cbor_format() { - let external_file = "/tmp/test_save_cbor_format.bin"; - let test_file = "/tmp/test_save_cbor_format.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - let _cleanup2 = defer(|| { - let filename = PathBuf::from(external_file) - .canonicalize() - .unwrap_or_else(|_| external_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - let sconfig = SConfig::builder() - .role( - SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task( - STask::builder("test_task") - .cred(SCredentials::builder().setuid(0).setgid(0).build()) - .commands( - SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) - .build(), - ) - .build(), - ) - .build(), - ) - .build(); - let settings = Rc::new(RefCell::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::CBOR) - .settings( - RemoteStorageSettings::builder() - .path(external_file) - .not_immutable() - .build(), - ) - .build(), - ) - .config(sconfig) - .build(), - )); - let file = File::create(test_file).unwrap(); - let file = Flock::lock(file, nix::fcntl::FlockArg::LockExclusive).unwrap(); - let mut settingsfile = LockedSettingsFile { - path: PathBuf::from(test_file), - fd: file, - data: settings, - }; - settingsfile.save().unwrap(); - //asset that external_file is a binary file - let mut file = read_with_privileges(external_file).unwrap(); - // try to parse as ciborium - let mut content = Vec::new(); - file.read_to_end(&mut content).unwrap(); - let deserialized: Versioning>> = - cbor4ii::serde::from_reader(&content[..]).unwrap(); - assert_eq!(deserialized.version, PACKAGE_VERSION); - fs::remove_file(test_file).unwrap(); - fs::remove_file(external_file).unwrap(); - } - - #[test] - fn test_locked_settings_file_open_new_file() { - let test_file = "/tmp/test_locked_settings_file_open_new_file.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - - // Test opening a non-existent file with write=false - let locked_file = LockedSettingsFile::open( - test_file, - &std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .to_owned(), - false, - ) - .unwrap(); - - // Should create default settings - assert_eq!(locked_file.path, PathBuf::from(test_file)); - assert_eq!(*locked_file.data.borrow(), FullSettings::default()); - } - - #[test] - fn test_locked_settings_file_open_existing_file() { - let test_file = "/tmp/test_locked_settings_file_open_existing_file.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - - // Create a test file with some content - let config = Versioning::new(Rc::new(RefCell::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .settings( - RemoteStorageSettings::builder() - .path(test_file) - .not_immutable() - .build(), - ) - .build(), - ) - .config( - SConfig::builder() - .role( - SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task( - STask::builder("test_task") - .cred(SCredentials::builder().setuid(0).setgid(0).build()) - .commands( - SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple( - "/usr/bin/true".to_string(), - )]) - .build(), - ) - .build(), - ) - .build(), - ) - .build(), - ) - .build(), - ))); - - let mut file = File::create(test_file).unwrap(); - write_json_config(&config, &mut file).unwrap(); - drop(file); - - // Test opening existing file - let locked_file = LockedSettingsFile::open( - test_file, - &std::fs::OpenOptions::new() - .read(true) - .write(true) - .to_owned(), - false, - ) - .unwrap(); - - // Should load the existing settings - assert_eq!(locked_file.path, PathBuf::from(test_file)); - assert_eq!(*locked_file.data.borrow(), *config.data.borrow()); - } - - #[test] - fn test_locked_settings_file_open_write_mode_non_immutable() { - let test_file = "/tmp/test_locked_settings_file_open_write_mode_non_immutable.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - - // Create a test file with non-immutable settings - let config = Versioning::new(Rc::new(RefCell::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .settings( - RemoteStorageSettings::builder() - .path(test_file) - .not_immutable() // explicitly not immutable - .build(), - ) - .build(), - ) - .build(), - ))); - - let mut file = File::create(test_file).unwrap(); - write_json_config(&config, &mut file).unwrap(); - drop(file); - - // Test opening existing file with write=true - should work normally for non-immutable files - let result = LockedSettingsFile::open( - test_file, - &std::fs::OpenOptions::new() - .read(true) - .write(true) - .to_owned(), - true, // write mode - ); - match result { - Ok(locked_file) => { - assert_eq!(locked_file.path, PathBuf::from(test_file)); - // The loaded settings should match our created config - assert_eq!( - locked_file.data.borrow().storage, - config.data.borrow().storage - ); - } - Err(_) => { - println!("Test skipped due to insufficient privileges in test environment"); - } - } - } - - #[test] - fn test_locked_settings_file_open_with_separate_config() { - let test_file = "/tmp/test_locked_settings_file_open_with_separate_config.json"; - let external_file = - "/tmp/test_locked_settings_file_open_with_separate_config_external.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - let _cleanup2 = defer(|| { - let filename = PathBuf::from(external_file) - .canonicalize() - .unwrap_or_else(|_| external_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - - // Create external config file - let sconfig = SConfig::builder() - .role( - SRole::builder("test_role") - .actor(SActor::user(0).build()) - .task( - STask::builder("test_task") - .cred(SCredentials::builder().setuid(0).setgid(0).build()) - .commands( - SCommands::builder(SetBehavior::None) - .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) - .build(), - ) - .build(), - ) - .build(), - ) - .build(); - let mut external_file_handle = File::create(external_file).unwrap(); - write_json_config(&Versioning::new(sconfig.clone()), &mut external_file_handle).unwrap(); - drop(external_file_handle); - - // Create settings file pointing to external config - let settings_config = Versioning::new(Rc::new(RefCell::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .settings( - RemoteStorageSettings::builder() - .path(external_file) - .not_immutable() - .build(), - ) - .build(), - ) - .build(), - ))); - let mut file = File::create(test_file).unwrap(); - write_json_config(&settings_config, &mut file).unwrap(); - drop(file); - - // Test opening file with separate config - let locked_file = LockedSettingsFile::open( - test_file, - &std::fs::OpenOptions::new() - .read(true) - .write(true) - .to_owned(), - false, - ) - .unwrap(); - - // Should load settings and external config - assert_eq!(locked_file.path, PathBuf::from(test_file)); - assert_eq!( - locked_file.data.borrow().storage, - settings_config.data.borrow().storage - ); - assert_eq!( - *locked_file.data.borrow().config.as_ref().unwrap().borrow(), - *sconfig.borrow() - ); - } - - #[test] - fn test_locked_settings_file_open_invalid_json() { - let test_file = "/tmp/test_locked_settings_file_open_invalid_json.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - - // Create a file with invalid JSON - let mut file = File::create(test_file).unwrap(); - file.write_all(b"{ invalid json content }").unwrap(); - drop(file); - - // Test opening file with invalid JSON - let locked_file = LockedSettingsFile::open( - test_file, - &std::fs::OpenOptions::new() - .read(true) - .write(true) - .to_owned(), - false, - ) - .unwrap(); - - // Should fall back to default settings when JSON is invalid - assert_eq!(locked_file.path, PathBuf::from(test_file)); - assert_eq!(*locked_file.data.borrow(), FullSettings::default()); - } - - #[test] - fn test_locked_settings_file_open_readonly() { - let test_file = "/tmp/test_locked_settings_file_open_readonly.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - - // Create a test file with minimal settings (no embedded config) - let config = Versioning::new(Rc::new(RefCell::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .build(), - ) - .build(), - ))); - - let mut file = File::create(test_file).unwrap(); - write_json_config(&config, &mut file).unwrap(); - drop(file); - - // Test opening file in read-only mode - let locked_file = LockedSettingsFile::open( - test_file, - &std::fs::OpenOptions::new().read(true).to_owned(), - false, // not write mode - ) - .unwrap(); - - // Should successfully open and load settings - assert_eq!(locked_file.path, PathBuf::from(test_file)); - // The storage settings should match what we wrote - assert_eq!( - locked_file.data.borrow().storage.method, - config.data.borrow().storage.method - ); - assert_eq!( - locked_file.data.borrow().storage.settings, - config.data.borrow().storage.settings - ); - // Config might be populated with defaults even if we didn't write any, so we don't assert on it - } - - #[test] - fn test_locked_settings_file_open_nonexistent_file_error() { - let test_file = "/tmp/test_locked_settings_file_open_nonexistent_file_error.json"; - - // Ensure the file doesn't exist - let _ = std::fs::remove_file(test_file); - - // Test opening non-existent file without create option - should fail - let result = LockedSettingsFile::open( - test_file, - &std::fs::OpenOptions::new().read(true).to_owned(), // No create flag - false, - ); - - // Should fail because file doesn't exist and we didn't set create=true - assert!(result.is_err()); - } - - #[test] - fn test_locked_settings_file_open_create_new() { - let test_file = "/tmp/test_locked_settings_file_open_create_new.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - - // Ensure the file doesn't exist - let _ = std::fs::remove_file(test_file); - - // Test creating a new file - let locked_file = LockedSettingsFile::open( - test_file, - &std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .to_owned(), - true, // write mode - ) - .unwrap(); - - // Should create new file with default settings - assert_eq!(locked_file.path, PathBuf::from(test_file)); - // File should exist now - assert!(PathBuf::from(test_file).exists()); - } - - #[test] - fn test_locked_settings_truncates_file_on_save() { - if has_privileges(&[Cap::LINUX_IMMUTABLE]).is_ok_and(|b| !b) { - println!("Test skipped due to insufficient privileges in test environment"); - return; - } - let test_file = "/tmp/test_locked_settings_truncates_file_on_save.json"; - let _cleanup = defer(|| { - let filename = PathBuf::from(test_file) - .canonicalize() - .unwrap_or_else(|_| test_file.into()); - if std::fs::remove_file(&filename).is_err() { - debug!("Failed to delete the file: {}", filename.display()); - } - }); - - // Create a test file with some initial content - let initial_content = r#"{ - "version": "0.1.0", - "storage": { - "method": "JSON" - }, - "config": { - "roles": [ - { - "name": "old_role", - "actors": [], - "tasks": [] - }, - { - "name": "another_old_role", - "actors": [], - "tasks": [] - }, - { - "name": "yet_another_old_role", - "actors": [], - "tasks": [] - }, - { - "name": "oldest_role", - "actors": [], - "tasks": [] - } - ] - } - }"#; - let mut file = File::create(test_file).unwrap(); - with_mutable_config(&mut file, |file| { - file.write_all(initial_content.as_bytes()).unwrap(); - Ok(()) - }) - .unwrap(); - - // Open the file using LockedSettingsFile - let mut locked = LockedSettingsFile::open( - test_file, - &std::fs::OpenOptions::new() - .read(true) - .write(true) - .to_owned(), - true, // write mode - ) - .unwrap(); - - // Modify the settings to have fewer roles - let new_config = SConfig::builder().build(); - locked.data.borrow_mut().config = Some(new_config); - locked.save().unwrap(); - // Read back the file content - let mut file = File::open(test_file).unwrap(); - let mut content = String::new(); - file.read_to_string(&mut content).unwrap(); - // The content should match the new settings and not contain old roles - assert!(!content.contains("old_role")); - assert!(!content.contains("another_old_role")); - assert!(!content.contains("yet_another_old_role")); - assert!(!content.contains("oldest_role")); - unlock_immutable(&mut file).unwrap(); - fs::remove_file(test_file).unwrap(); - } -} +#[cfg(feature = "ldap")] +compile_error!("LDAP feature is not yet implemented"); diff --git a/rar-common/src/lib2.rs b/rar-common/src/lib2.rs new file mode 100644 index 00000000..086cc50c --- /dev/null +++ b/rar-common/src/lib2.rs @@ -0,0 +1,419 @@ +#[derive(Debug, Builder)] +#[allow(clippy::missing_errors_doc)] +pub struct Cred { + #[builder(with = || -> Result<_,IoError> { + let uid = Uid::current(); + User::from_uid(uid)?.ok_or_else(|| IoError::other("User not found"))}) + ] + pub user: User, + #[builder(with = || -> Result<_,IoError> { + Ok(getgroups()? + .iter() + .map(|gid| Either::from(Group::from_gid(*gid).ok().flatten().ok_or(*gid))) + .collect()) + })] + pub groups: Vec>, + pub tty: Option, + #[builder(default = nix::unistd::getppid(), into)] + pub ppid: Pid, + #[builder(with = || -> Result<_, std::io::Error> { std::env::current_dir() })] + pub curdir: PathBuf, +} + +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Default, + Copy, + EnumString, + strum::VariantNames, + strum::EnumIs, +)] +#[serde(rename_all = "lowercase")] +#[repr(u8)] +pub enum StorageMethod { + #[default] + #[strum(ascii_case_insensitive)] + JSON, + #[strum(ascii_case_insensitive)] + CBOR, + // SQLite, + // PostgreSQL, + // MySQL, + // LDAP, +} + +type ConfigMap = BTreeMap; + +pub struct LockedSettingsFile { + path: PathBuf, + fd: Flock, // file descriptor to the opened file, to keep the lock + pub data: Rc>, +} + +pub struct RootSettings { + pub storage: SettingsContent, + pub config: Rc>, + pub nested: Rc>, +} + +type ConfigMap = BTreeMap; + +pub struct LockedSettingsFile { + path: PathBuf, + fd: Flock, // file descriptor to the opened file, to keep the lock + pub data: Rc>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Builder, PartialEq, Eq, Default)] +pub struct Settings { + pub storage: SettingsContent, +} + +#[derive(Debug, Clone, Builder, PartialEq, Eq, Default)] +pub struct FullSettings { + pub storage: SettingsContent, + #[builder(default = Rc::new(RefCell::new(BTreeMap::new())))] + pub config: Rc>, +} + +impl Serialize for FullSettings { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("storage", &self.storage)?; + // Flatten config fields into the main object + if let Some(config) = &self.config { + let config_value = + serde_json::to_value(&*config.borrow()).map_err(serde::ser::Error::custom)?; + if let serde_json::Value::Object(obj) = config_value { + for (key, value) in obj { + map.serialize_entry(&key, &value)?; + } + } + } + map.end() + } +} + +impl<'de> Deserialize<'de> for FullSettings { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct FullSettingsVisitor; + + impl<'de> serde::de::Visitor<'de> for FullSettingsVisitor { + type Value = FullSettings; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("struct FullSettings") + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut storage = None; + let mut config_fields = std::collections::HashMap::new(); + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "storage" | "s" => { + if storage.is_some() { + return Err(serde::de::Error::duplicate_field("storage")); + } + storage = Some(map.next_value()?); + } + // Collect all other fields as potential config fields + _ => { + config_fields.insert(key, map.next_value::()?); + } + } + } + + let storage = storage.ok_or_else(|| serde::de::Error::missing_field("storage"))?; + + // If we have multi-file configs, deserialize them + let config = if let Some(configs_json) = configs_value { + let mut config_map: BTreeMap = BTreeMap::new(); + if let serde_json::Value::Object(configs_obj) = configs_json { + for (path_str, config_value) in configs_obj { + let path = PathBuf::from(path_str); + let config_sconfig = SPolicy::deserialize(config_value) + .map_err(serde::de::Error::custom)?; + config_map.insert(path, config_sconfig); + } + } + Rc::new(RefCell::new(config_map)) + } else if !config_fields.is_empty() { + // Single config file embedded in settings + let config_value = + serde_json::Value::Object(config_fields.into_iter().collect()); + let single_config = + SPolicy::deserialize(config_value).map_err(serde::de::Error::custom)?; + let mut config_map = BTreeMap::new(); + config_map.insert(PathBuf::new(), single_config); + Rc::new(RefCell::new(config_map)) + } else { + // Empty config map + Rc::new(RefCell::new(BTreeMap::new())) + }; + + Ok(FullSettings { storage, config }) + } + } + + deserializer.deserialize_map(FullSettingsVisitor) + } +} + +// Default implementation for Settings +impl Default for SettingsContent { + fn default() -> Self { + Self { + method: StorageMethod::JSON, + settings: None, + ldap: None, + } + } +} + +#[builder] +fn write_storage_settings

( + path: P, + fd: &mut File, + method: StorageMethod, + config: &Versioning>>, + #[builder(default = false)] set_read_only: bool, + #[builder(default = false)] set_root_owner: bool, +) -> std::io::Result<()> +where + P: AsRef, +{ + debug!( + "Saving in {} : {}", + path.as_ref().display(), + serde_json::to_string_pretty(&config) + .unwrap_or_else(|_| "Failed to serialize config".to_string()) + ); + match method { + StorageMethod::JSON => write_json_config(config, fd), + StorageMethod::CBOR => write_cbor_config(config, fd), + }?; + if set_read_only { + if Uid::current().as_raw() == path.as_ref().metadata()?.uid() { + let perms = Permissions::from_mode(0o400); + std::fs::set_permissions(path.as_ref(), perms)?; + } else { + with_privileges(&[Cap::FOWNER], || { + let perms = Permissions::from_mode(0o400); + std::fs::set_permissions(path.as_ref(), perms) + })?; + } + } + if set_root_owner { + with_privileges(&[Cap::CHOWN], || { + nix::unistd::chown( + path.as_ref(), + Some(Uid::from_raw(0)), + Some(Gid::from_raw(0)), + ) + .map_err(|e| std::io::Error::from_raw_os_error(e as i32)) + })?; + } + Ok(()) +} + +fn write_sconfig_to_file

( + path: P, + fd: &mut File, + method: StorageMethod, + config: &Versioning, + set_read_only: bool, + set_root_owner: bool, +) -> std::io::Result<()> +where + P: AsRef, +{ + debug!( + "Saving in {} : {}", + path.as_ref().display(), + serde_json::to_string_pretty(&config) + .unwrap_or_else(|_| "Failed to serialize config".to_string()) + ); + match method { + StorageMethod::JSON => write_json_config(config, fd), + StorageMethod::CBOR => write_cbor_config(config, fd), + }?; + if set_read_only { + if Uid::current().as_raw() == path.as_ref().metadata()?.uid() { + let perms = Permissions::from_mode(0o400); + std::fs::set_permissions(path.as_ref(), perms)?; + } else { + with_privileges(&[Cap::FOWNER], || { + let perms = Permissions::from_mode(0o400); + std::fs::set_permissions(path.as_ref(), perms) + })?; + } + } + if set_root_owner { + with_privileges(&[Cap::CHOWN], || { + nix::unistd::chown( + path.as_ref(), + Some(Uid::from_raw(0)), + Some(Gid::from_raw(0)), + ) + .map_err(|e| std::io::Error::from_raw_os_error(e as i32)) + })?; + } + Ok(()) +} + +/// # Errors +/// Returns an error if the file cannot be opened or deserialized +pub fn read_full_settings(path: &S) -> Result>, Box> +where + S: AsRef, +{ + // if user does not have read permission, try to enable privilege + let file = read_with_privileges(path.as_ref())?; + load_full_settings(path, &file) +} + +/// # Errors +/// Returns an error if the directory cannot be read or config files cannot be deserialized +fn load_config_from_folder( + folder_path: &Path, + storage_method: StorageMethod, +) -> Result>, Box> { + let mut config_map = BTreeMap::new(); + + if !folder_path.is_dir() { + return Err(format!("Path is not a directory: {}", folder_path.display()).into()); + } + + // Iterate over files in the folder + for entry in std::fs::read_dir(folder_path)? { + let entry = entry?; + let path = entry.path(); + + if !path.is_file() { + continue; + } + + // Load the config file + match load_config_file(&path, storage_method) { + Ok(config) => { + debug!("Loaded config from: {}", path.display()); + config_map.insert(path, config); + } + Err(e) => { + warn!("Failed to load config from {}: {}", path.display(), e); + // Continue loading other files instead of failing completely + } + } + } + + Ok(Rc::new(RefCell::new(config_map))) +} + +/// # Errors +/// Returns an error if the file cannot be read or deserialized +fn load_config_file( + file_path: &Path, + storage_method: StorageMethod, +) -> Result> { + let file = read_with_privileges(file_path)?; + let value: Versioning = match storage_method { + StorageMethod::JSON => serde_json::from_reader(file)?, + StorageMethod::CBOR => cbor4ii::serde::from_reader(BufReader::new(file))?, + }; + debug!( + "Loaded config file: {}", + serde_json::to_string_pretty(&value)? + ); + Ok(value.data) +} + +/// # Errors +/// Returns an error if the file cannot be opened or deserialized +fn load_full_settings>( + path: &S, + file: &File, +) -> Result>, Box> { + let value: Versioning = serde_json::from_reader(file).inspect_err(|e| { + debug!("Error reading file: {e}"); + })?; + let settingsfile = rc_refcell!(value.data); + debug!("settingsfile: {settingsfile:?}"); + let default_remote = RemoteStorageSettings::default(); + let into = env!("RAR_CFG_DATA_PATH").to_string().into(); + { + let mut binding = settingsfile.as_ref().borrow_mut(); + let data_path = binding + .storage + .settings + .as_ref() + .unwrap_or(&default_remote) + .path + .as_ref() + .unwrap_or(&into); + if data_path != path.as_ref() { + // Check if path is a directory (multi-file config) or a file (single file config) + if data_path.is_dir() { + binding.config = load_config_from_folder(data_path, binding.storage.method)?; + } else { + // Load single file config + let single_config = retrieve_sconfig(&binding.storage.method, data_path)?; + + // Create a BTreeMap with a single entry + let mut config_map = BTreeMap::new(); + let config_clone = single_config.borrow().clone(); + config_map.insert(data_path.to_path_buf(), config_clone); + binding.config = Rc::new(RefCell::new(config_map)); + + // Make weak references for roles in the single config + make_weak_config(&single_config); + } + } else { + // Config is embedded in the settings file + // The config map should already be populated from deserialization + } + } + Ok(settingsfile) +} + +/// # Errors +/// Returns an error if the migration fails +pub fn migrate_settings(settings: &mut FullSettings) -> Result<(), Box> { + Migration::migrate(&PACKAGE_VERSION, settings, SETTINGS_MIGRATIONS)?; + Ok(()) +} + +/// # Errors +/// Returns an error if the file cannot be opened +pub fn get_settings(path: &S) -> Result> +where + S: AsRef, +{ + // if user does not have read permission, try to enable privilege + let file = read_with_privileges(path.as_ref())?; + let value: Versioning = serde_json::from_reader(file) + .inspect_err(|e| { + debug!("Error reading file: {e}"); + }) + .unwrap_or_else(|_| { + warn!("Using default settings file!!"); + Versioning::default() + }); + //read_effective(false).or(dac_override_effective(false))?; + debug!("{}", serde_json::to_string_pretty(&value)?); + Ok(value.data) +} diff --git a/rar-common/src/util.rs b/rar-common/src/util.rs index b3b07b52..098376f8 100644 --- a/rar-common/src/util.rs +++ b/rar-common/src/util.rs @@ -9,15 +9,19 @@ use capctl::{Cap, CapSet, ParseCapError}; use capctl::{CapState, prctl}; use chrono::Duration; -use konst::{iter, option, primitive::parse_i64, result, slice, string, unwrap_ctx}; +use konst::{eq_str, iter, option, result, string}; use libc::{FS_IOC_GETFLAGS, FS_IOC_SETFLAGS}; use log::{debug, warn}; -use nix::fcntl::{Flock, FlockArg}; -use serde::Serialize; +use nix::{ + fcntl::{Flock, FlockArg}, + unistd::{Gid, Group}, +}; +use serde::{Deserialize, Serialize}; +use strum::EnumString; use crate::database::options::{ EnvBehavior, PathBehavior, SAuthentication, SBounding, SInfo, SPrivileged, SUMask, - TimestampType, + TimestampType, WorkdirBehavior, }; #[cfg(feature = "finder")] @@ -37,16 +41,33 @@ pub const HARDENED_ENUM_VALUE_2: u32 = 0x69d6_1fc8; // 1101001110101100001111111 pub const HARDENED_ENUM_VALUE_3: u32 = 0x1629_e037; // 0010110001010011110000000110111 pub const HARDENED_ENUM_VALUE_4: u32 = 0x1fc8_d3ac; // 11111110010001101001110101100 +#[cfg(not(test))] +pub(super) const RAR_CFG_PATH: &str = env!("RAR_CFG_PATH"); +#[cfg(test)] +pub(super) const RAR_CFG_PATH: &str = "target/rootasrole.json"; + +#[cfg(not(test))] +pub const RAR_CFG_DATA_PATH: &str = env!("RAR_CFG_DATA_PATH"); +#[cfg(test)] +pub const RAR_CFG_DATA_PATH: &str = "target/rootasrole.json"; + +#[cfg(debug_assertions)] +pub(super) const RAR_CFG_IMMUTABLE: bool = false; +#[cfg(not(debug_assertions))] +pub(super) const RAR_CFG_IMMUTABLE: bool = eq_str(env!("RAR_CFG_IMMUTABLE"), "true"); + +pub const RAR_CFG_TYPE: StorageMethod = StorageMethod::const_parse(env!("RAR_CFG_TYPE")); + pub const ENV_PATH_BEHAVIOR: PathBehavior = PathBehavior::const_parse(env!("RAR_PATH_DEFAULT")); pub const ENV_PATH_ADD_LIST_SLICE: &[&str] = &iter::collect_const!(&str => string::split(env!("RAR_PATH_ADD_LIST"), ":"), - map(string::trim), + map(str::trim_ascii), ); pub const ENV_PATH_REMOVE_LIST_SLICE: &[&str] = &iter::collect_const!(&str => string::split(env!("RAR_PATH_REMOVE_LIST"), ":"), - map(string::trim), + map(str::trim_ascii), ); //=== ENV === @@ -54,24 +75,24 @@ pub const ENV_DEFAULT_BEHAVIOR: EnvBehavior = EnvBehavior::const_parse(env!("RAR pub const ENV_KEEP_LIST_SLICE: &[&str] = &iter::collect_const!(&str => string::split(env!("RAR_ENV_KEEP_LIST"), ","), - map(string::trim), + map(str::trim_ascii), ); pub const ENV_CHECK_LIST_SLICE: &[&str] = &iter::collect_const!(&str => string::split(env!("RAR_ENV_CHECK_LIST"), ","), - map(string::trim), + map(str::trim_ascii), ); pub const ENV_DELETE_LIST_SLICE: &[&str] = &iter::collect_const!(&str => string::split(env!("RAR_ENV_DELETE_LIST"), ","), - map(string::trim), + map(str::trim_ascii), ); pub const ENV_SET_LIST_SLICE: &[(&str, &str)] = &iter::collect_const!((&str, &str) => string::split(env!("RAR_ENV_SET_LIST"), "\n"), filter_map(|s| { if let Some((key,value)) = string::split_once(s, '=') { - Some((string::trim(key),string::trim(value))) + Some((str::trim_ascii(key),str::trim_ascii(value))) } else { None } @@ -83,17 +104,17 @@ pub const ENV_OVERRIDE_BEHAVIOR: bool = result::unwrap_or!( false ); -pub static ENV_KEEP_LIST: [&str; ENV_KEEP_LIST_SLICE.len()] = - *unwrap_ctx!(slice::try_into_array(ENV_KEEP_LIST_SLICE)); +pub static ENV_KEEP_LIST: &[&str; ENV_KEEP_LIST_SLICE.len()] = + result::unwrap!(konst::slice::try_into_array(ENV_KEEP_LIST_SLICE)); -pub static ENV_CHECK_LIST: [&str; ENV_CHECK_LIST_SLICE.len()] = - *unwrap_ctx!(slice::try_into_array(ENV_CHECK_LIST_SLICE)); +pub static ENV_CHECK_LIST: &[&str; ENV_CHECK_LIST_SLICE.len()] = + result::unwrap!(konst::slice::try_into_array(ENV_CHECK_LIST_SLICE)); -pub static ENV_DELETE_LIST: [&str; ENV_DELETE_LIST_SLICE.len()] = - *unwrap_ctx!(slice::try_into_array(ENV_DELETE_LIST_SLICE)); +pub static ENV_DELETE_LIST: &[&str; ENV_DELETE_LIST_SLICE.len()] = + result::unwrap!(konst::slice::try_into_array(ENV_DELETE_LIST_SLICE)); -pub static ENV_SET_LIST: [(&str, &str); ENV_SET_LIST_SLICE.len()] = - *unwrap_ctx!(slice::try_into_array(ENV_SET_LIST_SLICE)); +pub static ENV_SET_LIST: &[(&str, &str); ENV_SET_LIST_SLICE.len()] = + result::unwrap!(konst::slice::try_into_array(ENV_SET_LIST_SLICE)); //=== STimeout === @@ -107,6 +128,122 @@ pub const TIMEOUT_DURATION: Duration = option::unwrap_or!( Duration::seconds(5) ); +pub const WORKDIR_BEHAVIOR: WorkdirBehavior = + assert_valid_workdir_behavior(WorkdirBehavior::const_parse(env!("RAR_WORKDIR_BEHAVIOR"))); + +const fn assert_valid_workdir_behavior(e: WorkdirBehavior) -> WorkdirBehavior { + match e { + WorkdirBehavior::Inherit => panic!("Workdir behavior cannot be inherit"), + e => e, + } +} + +pub const WORKDIR_FALLBACK: Option<&str> = option_env!("RAR_WORKDIR_FALLBACK"); + +pub const WORKDIR_ADD_LIST_SLICE: &[&str] = &iter::collect_const!(&str => + string::split(env!("RAR_WORKDIR_ADD_LIST"), ","), + map(str::trim_ascii), +); + +pub const WORKDIR_REMOVE_LIST_SLICE: &[&str] = &iter::collect_const!(&str => + string::split(env!("RAR_WORKDIR_REMOVE_LIST"), ","), + map(str::trim_ascii), +); + +pub static WORKDIR_ADD_LIST: &[&str; WORKDIR_ADD_LIST_SLICE.len()] = + result::unwrap!(konst::slice::try_into_array(WORKDIR_ADD_LIST_SLICE)); + +pub static WORKDIR_REMOVE_LIST: &[&str; WORKDIR_REMOVE_LIST_SLICE.len()] = + result::unwrap!(konst::slice::try_into_array(WORKDIR_REMOVE_LIST_SLICE)); + +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + PartialEq, + Eq, + Copy, + EnumString, + strum::VariantNames, + strum::EnumIs, + strum::Display, +)] +#[serde(rename_all = "lowercase")] +#[repr(u8)] +pub enum StorageMethod { + #[strum(ascii_case_insensitive)] + JSON, + #[strum(ascii_case_insensitive)] + CBOR, + // SQLite, + // PostgreSQL, + // MySQL, +} + +impl Default for StorageMethod { + fn default() -> Self { + RAR_CFG_TYPE + } +} + +impl StorageMethod { + /// # Panics + /// Panics if the string does not correspond to a valid storage method. + #[must_use] + pub const fn const_parse(s: &str) -> Self { + match s { + _ if eq_str(s, "cbor") => Self::CBOR, + _ if eq_str(s, "json") => Self::JSON, + _ => panic!("fail to parse StorageMethod from string: invalid value"), + } + } +} + +/// `Either` is a type that represents either type A ([`Left`]) or type B ([`Right`]). +#[derive(Debug, Hash, Copy, Clone)] +#[must_use] +pub enum Either { + /// Contains the Left value + Left(L), + /// Contains the Right value + Right(R), +} +impl Either { + pub const fn left(&self) -> Option<&L> { + match self { + Self::Left(l) => Some(l), + Self::Right(_) => None, + } + } + pub const fn right(&self) -> Option<&R> { + match self { + Self::Left(_) => None, + Self::Right(r) => Some(r), + } + } + pub fn map(&self, left: impl Fn(&L) -> T, right: impl Fn(&R) -> T) -> T { + match self { + Self::Left(l) => left(l), + Self::Right(r) => right(r), + } + } +} + +impl From> for Either { + fn from(value: Result) -> Self { + match value { + Ok(l) => Self::Left(l), + Err(r) => Self::Right(r), + } + } +} + +#[must_use] +pub fn either_to_gid(either: &Either) -> Gid { + either.map(|l| l.gid, |r| *r) +} + #[derive(Debug)] struct DurationParseError; impl std::fmt::Display for DurationParseError { @@ -118,28 +255,28 @@ impl std::fmt::Display for DurationParseError { const fn convert_string_to_duration( s: &str, ) -> Result, DurationParseError> { - let parts = string::split(s, ':'); - let Some((hours, parts)) = parts.next() else { + let mut parts = string::split(s, ':'); + let Some(hours) = parts.next() else { return Err(DurationParseError); }; - let Some((minutes, parts)) = parts.next() else { + let Some(minutes) = parts.next() else { return Err(DurationParseError); }; - let Some((seconds, _)) = parts.next() else { + let Some(seconds) = parts.next() else { return Err(DurationParseError); }; - let hours: i64 = if let Ok(hours) = parse_i64(hours) { + let hours: i64 = if let Ok(hours) = i64::from_str_radix(hours, 10) { hours } else { return Err(DurationParseError); }; - let minutes: i64 = if let Ok(minutes) = parse_i64(minutes) { + let minutes: i64 = if let Ok(minutes) = i64::from_str_radix(minutes, 10) { minutes } else { return Err(DurationParseError); }; - let seconds: i64 = if let Ok(seconds) = parse_i64(seconds) { + let seconds: i64 = if let Ok(seconds) = i64::from_str_radix(seconds, 10) { seconds } else { return Err(DurationParseError); @@ -149,10 +286,8 @@ const fn convert_string_to_duration( ))) } -pub const TIMEOUT_MAX_USAGE: u64 = result::unwrap_or!( - konst::primitive::parse_u64(env!("RAR_TIMEOUT_MAX_USAGE")), - 0 -); +pub const TIMEOUT_MAX_USAGE: u64 = + result::unwrap_or!(u64::from_str_radix(env!("RAR_TIMEOUT_MAX_USAGE"), 10), 0); pub const BOUNDING: SBounding = SBounding::const_parse(env!("RAR_BOUNDING")); @@ -162,7 +297,7 @@ pub const AUTHENTICATION: SAuthentication = pub const PRIVILEGED: SPrivileged = SPrivileged::const_parse(env!("RAR_USER_CONSIDERED")); pub const UMASK: SUMask = SUMask(result::unwrap_or!( - konst::primitive::parse_u16(env!("RAR_UMASK")), + u16::from_str_radix(env!("RAR_UMASK"), 10), 0o022 )); @@ -422,7 +557,7 @@ pub fn match_single_path(cmd_path: &Path, role_path: &str) -> CmdMin { /// Returns an error if the logger fails to initialize pub fn subsribe(_: &str) -> io::Result<()> { env_logger::Builder::from_default_env() - .filter_level(log::LevelFilter::Debug) + .filter_level(log::LevelFilter::Trace) .format_module_path(true) .init(); Ok(()) @@ -497,14 +632,27 @@ pub fn activates_no_new_privs() -> Result<(), capctl::Error> { /// # Errors /// Returns an error if the internal write operation fails -pub fn write_json_config(settings: &T, file: &mut impl Write) -> std::io::Result<()> { +pub fn write_config( + settings: &T, + file: &mut impl Write, + method: StorageMethod, +) -> std::io::Result<()> { + match method { + StorageMethod::JSON => write_json_config(settings, file), + StorageMethod::CBOR => write_cbor_config(settings, file), + } +} + +/// # Errors +/// Returns an error if the internal write operation fails +fn write_json_config(settings: &T, file: &mut impl Write) -> std::io::Result<()> { serde_json::to_writer_pretty(file, &settings)?; Ok(()) } /// # Errors /// Returns an error if the internal write operation fails -pub fn write_cbor_config(settings: &T, file: &mut impl Write) -> std::io::Result<()> { +fn write_cbor_config(settings: &T, file: &mut impl Write) -> std::io::Result<()> { cbor4ii::serde::to_writer(file, &settings) .map_err(|e| std::io::Error::other(format!("Failed to write cbor config: {e}"))) } diff --git a/rar-exec/Cargo.toml b/rar-exec/Cargo.toml index 91b18cfe..d63270fd 100644 --- a/rar-exec/Cargo.toml +++ b/rar-exec/Cargo.toml @@ -16,3 +16,9 @@ log = "0.4" [dev-dependencies] serial_test = "3.2.0" +[lints.clippy] +pedantic = { level = "warn", priority = 1 } +nursery = { level = "warn", priority = 1 } +unwrap_used = { level = "deny", priority = 2 } +similar_names = { level = "allow", priority = 2 } +should_implement_trait = { level = "allow", priority = 2 } diff --git a/rar-exec/src/monitor/backchannel.rs b/rar-exec/src/monitor/backchannel.rs index 6367e5c0..997787b4 100644 --- a/rar-exec/src/monitor/backchannel.rs +++ b/rar-exec/src/monitor/backchannel.rs @@ -1,5 +1,6 @@ /// This backchannel uses rkyv instead of sudo-rs implementation because I don't want to maintain a custom serialization format /// and rkyv is clearly suitable and performant. I think serde would be too long to setup for this use case. +use log::{trace, warn}; use rkyv::{Archive, Deserialize, Serialize}; use std::io::{self, Write}; use std::os::unix::net::UnixStream; @@ -10,29 +11,47 @@ macro_rules! signals { (core + [$($extra:expr),* $(,)?]) => { &[ libc::SIGINT, - libc::SIGTERM, libc::SIGQUIT, + libc::SIGTSTP, + libc::SIGTERM, libc::SIGHUP, libc::SIGCONT, - libc::SIGTSTP, + libc::SIGALRM, $($extra),* ] }; } /// Signals that can be forwarded from runner to command. -pub const FORWARDABLE_SIGNALS: &[libc::c_int] = signals!(core + [libc::SIGUSR1, libc::SIGUSR2]); +pub const FORWARDABLE_SIGNALS: &[libc::c_int] = + signals!(core + [libc::SIGUSR1, libc::SIGUSR2, libc::SIGWINCH]); /// Signals to register for no-pty execution (runner). -pub const RUNNER_SIGNALS_NO_PTY: &[libc::c_int] = signals!(core + [libc::SIGCHLD]); +pub const RUNNER_SIGNALS_NO_PTY: &[libc::c_int] = signals!( + core + [ + libc::SIGPIPE, + libc::SIGUSR1, + libc::SIGUSR2, + libc::SIGCHLD, + libc::SIGWINCH + ] +); /// Signals to register for pty execution (runner). pub const RUNNER_SIGNALS_WITH_PTY: &[libc::c_int] = - signals!(core + [libc::SIGWINCH, libc::SIGCHLD]); + signals!(core + [libc::SIGUSR1, libc::SIGUSR2, libc::SIGCHLD, libc::SIGWINCH]); /// Signals to register for monitor process. -pub const MONITOR_SIGNALS: &[libc::c_int] = - signals!(core + [libc::SIGUSR1, libc::SIGUSR2, libc::SIGCHLD]); +pub const MONITOR_SIGNALS: &[libc::c_int] = &[ + libc::SIGINT, + libc::SIGQUIT, + libc::SIGTSTP, + libc::SIGTERM, + libc::SIGHUP, + libc::SIGUSR1, + libc::SIGUSR2, + libc::SIGCHLD, +]; #[inline] #[must_use] @@ -64,11 +83,13 @@ pub struct Backchannel { impl Backchannel { /// # Errors - /// Returns an error if the ``UnixStream`` pair cannot be created or if setting non-blocking + /// Returns an error if the ``UnixStream`` pair cannot be created + /// + /// The backchannel sockets are created as BLOCKING. They will be registered with `poll()`, + /// which will notify when they're ready. Then they can be read without blocking the event loop. pub fn pair() -> io::Result<(Self, Self)> { let (a, b) = UnixStream::pair()?; - a.set_nonblocking(true)?; - b.set_nonblocking(true)?; + // Keep both as blocking - poll() will notify when data is available Ok((Self { stream: a }, Self { stream: b })) } @@ -81,6 +102,7 @@ impl Backchannel { } #[allow(clippy::cast_possible_truncation)] let len = (data.len() as u32).to_le_bytes(); + trace!("backchannel: write frame len={}", data.len()); self.stream.write_all(&len)?; self.stream.write_all(data)?; Ok(()) @@ -92,6 +114,7 @@ impl Backchannel { self.stream.read_exact(&mut len_buf)?; let len = u32::from_le_bytes(len_buf) as usize; if len == 0 || len > MAX_BACKCHANNEL_MESSAGE_SIZE { + warn!("backchannel: invalid frame size {len}"); return Err(io::Error::new( io::ErrorKind::InvalidData, format!("invalid backchannel message size: {len} bytes"), @@ -99,6 +122,7 @@ impl Backchannel { } let mut data = vec![0u8; len]; + trace!("backchannel: read frame len={len}"); self.stream.read_exact(&mut data)?; Ok(data) } @@ -108,6 +132,7 @@ impl Backchannel { pub fn send_monitor_message(&mut self, msg: &MonitorMessage) -> io::Result<()> { let data = rkyv::to_bytes::(msg) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + trace!("backchannel: send monitor message {msg:?}"); self.write_frame(&data) } @@ -116,8 +141,11 @@ impl Backchannel { pub fn recv_monitor_message(&mut self) -> io::Result { let data = self.read_frame()?; // SAFETY: bytes come from our own serializer over a trusted local socket. - unsafe { rkyv::from_bytes_unchecked::(&data) } - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) + let msg = + unsafe { rkyv::from_bytes_unchecked::(&data) } + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + trace!("backchannel: recv monitor message {msg:?}"); + Ok(msg) } /// # Errors @@ -125,6 +153,7 @@ impl Backchannel { pub fn send_parent_message(&mut self, msg: &ParentMessage) -> io::Result<()> { let data = rkyv::to_bytes::(msg) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + trace!("backchannel: send parent message {msg:?}"); self.write_frame(&data) } @@ -132,8 +161,10 @@ impl Backchannel { /// Returns an error if deserialization fails, if reading from the stream fails pub fn recv_parent_message(&mut self) -> io::Result { let data = self.read_frame()?; - unsafe { rkyv::from_bytes_unchecked::(&data) } - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) + let msg = rkyv::from_bytes::(&data) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; + trace!("backchannel: recv parent message {msg:?}"); + Ok(msg) } pub const fn get_mut(&mut self) -> &mut UnixStream { @@ -221,4 +252,31 @@ mod tests { .expect_err("zero-sized frame must fail"); assert_eq!(error.kind(), io::ErrorKind::InvalidData); } + + #[test] + fn backchannel_rejects_truncated_parent_message() { + let (mut sender, mut receiver) = Backchannel::pair().expect("create backchannel pair"); + + // Serialize a valid message first to get real serialized bytes + let msg = ParentMessage::CommandPid(4242); + let full_data = + rkyv::to_bytes::(&msg).expect("serialize parent message"); + + // Write the full length of the serialized data, but only send part of it + #[allow(clippy::cast_possible_truncation)] + let truncated_len = (full_data.len() / 2) as u32; + sender + .get_mut() + .write_all(&truncated_len.to_le_bytes()) + .expect("write truncated frame header"); + sender + .get_mut() + .write_all(&full_data[..truncated_len as usize]) + .expect("write partial frame payload"); + + let error = receiver + .recv_parent_message() + .expect_err("truncated frame must fail gracefully"); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } } diff --git a/rar-exec/src/monitor/mod.rs b/rar-exec/src/monitor/mod.rs index b95c5093..e192120e 100644 --- a/rar-exec/src/monitor/mod.rs +++ b/rar-exec/src/monitor/mod.rs @@ -6,10 +6,10 @@ use std::process::Command; use crate::event::{EventRegistry, PollEvent, Process}; use crate::orchestrator::{Orchestrator, PreExecContext}; use crate::pty::PtyFollower; -use crate::signal::{SignalStream, register_signal_handler}; +use crate::signal::{SignalInfo, SignalStream, register_signal_handler}; use crate::terminal::TerminalExt; use libc::{SIGCHLD, SIGKILL}; -use log::error; +use log::{debug, error, trace, warn}; pub mod backchannel; use self::backchannel::{ @@ -17,11 +17,14 @@ use self::backchannel::{ }; pub struct MonitorClosure { - command_pid: Option, + command_pid: Option, + command_pgrp: libc::pid_t, + monitor_pgrp: libc::pid_t, pty_follower: PtyFollower, signal_stream: &'static SignalStream, backchannel: Backchannel, err_reader: std::os::unix::net::UnixStream, + err_handle: crate::event::EventHandle, } #[derive(Clone, Copy, Debug)] @@ -42,9 +45,14 @@ impl Process for MonitorClosure { use std::io::Read; let mut buf = [0u8; 1024]; match self.err_reader.read(&mut buf) { - Ok(0) => {} // EOF, no error + Ok(0) => { + // EOF: stop polling this FD to avoid busy loop + trace!("monitor: err pipe EOF"); + self.err_handle.ignore(registry); + } Ok(n) => { let err_msg = String::from_utf8_lossy(&buf[..n]).to_string(); + warn!("monitor: exec error: {err_msg}"); if let Err(e) = self .backchannel .send_parent_message(&ParentMessage::Error(err_msg.clone())) @@ -55,7 +63,7 @@ impl Process for MonitorClosure { if let Some(pid) = self.command_pid { let mut status = 0; - unsafe { libc::waitpid(pid.cast_signed(), &raw mut status, 0) }; + unsafe { libc::waitpid(pid, &raw mut status, 0) }; let _ = self .backchannel .send_parent_message(&ParentMessage::ExitStatus(status)); @@ -71,43 +79,44 @@ impl Process for MonitorClosure { } MonitorEvent::Signal => loop { match self.signal_stream.recv() { - Ok(info) => self.handle_signal(info.signal, registry), + Ok(info) => self.handle_signal(info, registry), Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break, Err(e) => { + warn!("monitor: signal recv error: {e}"); registry.set_break(e); break; } } }, MonitorEvent::Backchannel => { - loop { - match self.backchannel.recv_monitor_message() { - Ok(MonitorMessage::Signal(sig)) => { - if is_forward_signal_allowed(sig) { - if let Some(pid) = self.command_pid { - unsafe { libc::kill(pid.cast_signed(), sig) }; + trace!("monitor: backchannel readable"); + match self.backchannel.recv_monitor_message() { + Ok(MonitorMessage::Signal(sig)) => { + debug!("monitor: signal from parent {sig}"); + if is_forward_signal_allowed(sig) { + if let Some(pid) = self.command_pid { + if sig == libc::SIGALRM { + unsafe { libc::kill(pid, libc::SIGKILL) }; + } else { + unsafe { libc::kill(pid, sig) }; } - } else { - let _ = - self.backchannel.send_parent_message(&ParentMessage::Error( - format!("Rejected disallowed signal value: {sig}"), - )); } - } - Ok(MonitorMessage::Edge) => { - // unexpected. - registry.set_break(io::Error::new( - io::ErrorKind::InvalidData, - "Unexpected Edge message from parent".to_string(), + } else { + let _ = self.backchannel.send_parent_message(&ParentMessage::Error( + format!("Rejected disallowed signal value: {sig}"), )); - break; } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break, - Err(e) => { - registry.set_break(e); - break; + } + Ok(MonitorMessage::Edge) => { + if self.command_pid.is_none() { + debug!("monitor: stop edge received after command exit"); + registry.set_break(io::Error::from_raw_os_error(0)); } } + //Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { }, + Err(e) => { + registry.set_break(e); + } } } } @@ -115,29 +124,75 @@ impl Process for MonitorClosure { } impl MonitorClosure { - fn handle_signal(&mut self, signal: libc::c_int, registry: &mut EventRegistry) { - if signal == SIGCHLD { + fn handle_signal(&mut self, info: SignalInfo, registry: &mut EventRegistry) { + debug!("monitor: got signal {} from pid {}", info.signal, info.pid); + if info.signal == SIGCHLD { if let Some(pid) = self.command_pid { let mut status = 0; let res = - unsafe { libc::waitpid(pid.cast_signed(), &raw mut status, libc::WNOHANG) }; + unsafe { libc::waitpid(pid, &raw mut status, libc::WNOHANG | libc::WUNTRACED) }; if res > 0 { + if libc::WIFSTOPPED(status) { + warn!("monitor: command stopped with {}", libc::WSTOPSIG(status)); + let _ = + self.backchannel + .send_parent_message(&ParentMessage::Error(format!( + "Command stopped with signal {}", + libc::WSTOPSIG(status) + ))); + return; + } + self.command_pid = None; + debug!("monitor: command exited with status {status}"); let _ = self .backchannel .send_parent_message(&ParentMessage::ExitStatus(status)); - // Exit monitor loop naturally registry.set_break(io::Error::from_raw_os_error(0)); } } - } else if is_forward_signal_allowed(signal) - && let Some(pid) = self.command_pid - { - unsafe { libc::kill(pid.cast_signed(), signal) }; + return; + } + + if let Some(pid) = self.command_pid { + if info.pid > 0 && is_self_terminating(info.pid, pid, self.command_pgrp) { + trace!("monitor: ignoring self-terminating signal {}", info.signal); + return; + } + + if is_forward_signal_allowed(info.signal) { + debug!("monitor: forwarding signal {}", info.signal); + if info.signal == libc::SIGALRM { + unsafe { libc::kill(pid, libc::SIGKILL) }; + } else { + unsafe { libc::kill(pid, info.signal) }; + } + } } } } +/// Determines if a signal from `signaler_pid` should be considered self-terminating for the command process. +/// A signal is considered self-terminating if it originates from the command process itself or +/// from a process in the same process group. +fn is_self_terminating( + signaler_pid: libc::pid_t, + command_pid: libc::pid_t, + command_pgrp: libc::pid_t, +) -> bool { + if signaler_pid <= 0 { + return false; + } + + if signaler_pid == command_pid { + return true; + } + + let signaler_pgrp = unsafe { libc::getpgid(signaler_pid) }; + signaler_pgrp == command_pgrp +} + +#[allow(clippy::too_many_lines)] /// # Errors /// Returns an error if any system call fails during the monitor process execution. pub fn exec_monitor_process( @@ -146,12 +201,20 @@ pub fn exec_monitor_process( orchestrator: Orchestrator, mut backchannel: Backchannel, original_set: Option<&libc::sigset_t>, + foreground: bool, ) -> io::Result<()> { + debug!("monitor: starting (foreground={foreground})"); + unsafe { + libc::signal(libc::SIGTTIN, libc::SIG_IGN); + libc::signal(libc::SIGTTOU, libc::SIG_IGN); + } + if unsafe { libc::setsid() } == -1 { return Err(io::Error::last_os_error()); } pty_follower.as_tty()?.make_controlling_terminal()?; + debug!("monitor: controlling terminal set"); let f_fd = pty_follower.as_fd().as_raw_fd(); unsafe { @@ -175,6 +238,7 @@ pub fn exec_monitor_process( Err(e) => return Err(e), } } + debug!("monitor: received start edge"); backchannel.set_nonblocking(true)?; let (err_reader, mut err_writer) = std::os::unix::net::UnixStream::pair()?; @@ -209,8 +273,15 @@ pub fn exec_monitor_process( let _ = unsafe { libc::setpgid(0, 0) }; let cmd_pid = unsafe { libc::getpid() }; - while unsafe { libc::tcgetpgrp(pty_follower.as_fd().as_raw_fd()) } != cmd_pid { - std::thread::yield_now(); + if foreground { + let pgrp = unsafe { libc::tcgetpgrp(pty_follower.as_fd().as_raw_fd()) }; + if pgrp == cmd_pid { + debug!("monitor: foreground pgrp confirmed"); + } else { + warn!( + "monitor: foreground pgrp mismatch (expected {cmd_pid}, got {pgrp}), continuing exec" + ); + } } let err = command.exec(); @@ -228,32 +299,56 @@ pub fn exec_monitor_process( err_reader.set_nonblocking(true)?; let _ = unsafe { libc::setpgid(pid, pid) }; - let _ = unsafe { libc::tcsetpgrp(pty_follower.as_fd().as_raw_fd(), pid) }; + if foreground { + let res = unsafe { libc::tcsetpgrp(pty_follower.as_fd().as_raw_fd(), pid) }; + if res == -1 { + warn!("monitor: tcsetpgrp failed: {}", io::Error::last_os_error()); + } else { + debug!("monitor: set foreground pgrp to {pid}"); + } + } + debug!("monitor: command pid {pid}"); // Send PID to Runner let _ = backchannel.send_parent_message(&ParentMessage::CommandPid(pid)); + debug!("monitor: sent command pid to parent"); let mut registry = EventRegistry::new(); + + let err_handle = + registry.register_event(&err_reader, PollEvent::Readable, |_| MonitorEvent::ErrPipe); + let signal_stream = SignalStream::init()?; for &sig in MONITOR_SIGNALS { register_signal_handler(sig)?; } + debug!("monitor: signal handlers installed"); + + if let Some(set) = original_set { + unsafe { + libc::sigprocmask(libc::SIG_SETMASK, set, std::ptr::null_mut()); + } + debug!("monitor: signal mask restored"); + } registry.register_event(signal_stream, PollEvent::Readable, |_| MonitorEvent::Signal); registry.register_event(&backchannel, PollEvent::Readable, |_| { MonitorEvent::Backchannel }); - registry.register_event(&err_reader, PollEvent::Readable, |_| MonitorEvent::ErrPipe); let mut monitor = MonitorClosure { - command_pid: Some(pid.cast_unsigned()), + command_pid: Some(pid), + command_pgrp: pid, + monitor_pgrp: unsafe { libc::getpgrp() }, pty_follower, // Keep it alive signal_stream, backchannel, err_reader, + err_handle, }; // Monitor Loop let _ = registry.event_loop(&mut monitor); + debug!("monitor: event loop exited"); monitor.backchannel.set_nonblocking(false)?; // Blocking wait loop { @@ -265,16 +360,60 @@ pub fn exec_monitor_process( Err(_e) => break, // Error } } - - let monitor_pgrp = unsafe { libc::getpgrp() }; - let _ = unsafe { libc::tcsetpgrp(monitor.pty_follower.as_fd().as_raw_fd(), monitor_pgrp) }; + debug!("monitor: received stop edge or backchannel closed"); + + // Restore terminal foreground process group to monitor (prevent SIGHUP to children) + // This is critical to ensure the next invocation doesn't freeze waiting for foreground pgrp + let _ = unsafe { + libc::tcsetpgrp( + monitor.pty_follower.as_fd().as_raw_fd(), + monitor.monitor_pgrp, + ) + }; + debug!( + "monitor: restored foreground pgrp to monitor ({})", + monitor.monitor_pgrp + ); // Cleanup if let Some(pid) = monitor.command_pid { - unsafe { libc::kill(pid.cast_signed(), SIGKILL) }; + unsafe { libc::kill(pid, SIGKILL) }; let mut status = 0; - unsafe { libc::waitpid(pid.cast_signed(), &raw mut status, 0) }; + unsafe { libc::waitpid(pid, &raw mut status, 0) }; } Ok(()) } + +#[cfg(test)] +mod tests { + use super::is_self_terminating; + + #[test] + fn self_terminating_true_for_same_pid() { + let pid = unsafe { libc::getpid() }; + let pgrp = unsafe { libc::getpgrp() }; + assert!(is_self_terminating(pid, pid, pgrp)); + } + + #[test] + fn self_terminating_true_for_same_pgrp() { + let pid = unsafe { libc::getpid() }; + let pgrp = unsafe { libc::getpgrp() }; + assert!(is_self_terminating(pid, pid + 1, pgrp)); + } + + #[test] + fn self_terminating_false_for_other_pgrp() { + let pid = unsafe { libc::getpid() }; + let pgrp = unsafe { libc::getpgrp() }; + assert!(!is_self_terminating(pid, pid + 1, pgrp + 1)); + } + + #[test] + fn self_terminating_false_for_non_positive_pid() { + let pgrp = unsafe { libc::getpgrp() }; + assert!(!is_self_terminating(0, 1, pgrp)); + assert!(!is_self_terminating(-1, 1, pgrp)); + } +} diff --git a/rar-exec/src/pipe.rs b/rar-exec/src/pipe.rs index 6a36f323..00f6d3f9 100644 --- a/rar-exec/src/pipe.rs +++ b/rar-exec/src/pipe.rs @@ -4,6 +4,8 @@ use std::{ os::fd::AsFd, }; +use log::{debug, trace}; + use super::event::{EventHandle, EventRegistry, PollEvent, Process}; use ringbuf::HeapRb; @@ -107,6 +109,7 @@ impl Pipe { match poll_event { PollEvent::Readable => { let bytes_read = self.buffer_lr.read(&mut self.left, registry)?; + trace!("pipe: left readable -> {bytes_read} bytes"); if bytes_read == 0 { self.buffer_lr.read_handle.ignore(registry); } else if let Some(logger) = &mut self.logger { @@ -121,7 +124,9 @@ impl Pipe { Ok(()) } PollEvent::Writable => { - if self.buffer_rl.write(&mut self.left, registry)? { + let did_write = self.buffer_rl.write(&mut self.left, registry)?; + trace!("pipe: left writable -> wrote={did_write}"); + if did_write { self.buffer_rl.read_handle.resume(registry); } Ok(()) @@ -139,6 +144,7 @@ impl Pipe { match poll_event { PollEvent::Readable => { let bytes_read = self.buffer_rl.read(&mut self.right, registry)?; + trace!("pipe: right readable -> {bytes_read} bytes"); if bytes_read == 0 { self.buffer_rl.read_handle.ignore(registry); } else if let Some(logger) = &mut self.logger { @@ -155,6 +161,7 @@ impl Pipe { PollEvent::Writable => { match self.buffer_lr.write(&mut self.right, registry) { Ok(did_write) => { + trace!("pipe: right writable -> wrote={did_write}"); if did_write && !self.background { self.buffer_lr.read_handle.resume(registry); } @@ -177,6 +184,7 @@ impl Pipe { /// # Errors /// Returns an error if writing to the left pipe fails, or if logging fails. pub fn flush_left(&mut self) -> io::Result<()> { + debug!("pipe: flushing left"); let buffer = &mut self.buffer_rl; let source = &mut self.right; let sink = &mut self.left; @@ -185,22 +193,20 @@ impl Pipe { res?; } - if buffer.write_handle.is_active() { - let mut buf = [0u8; BUFFER_LEN]; - loop { - match source.read(&mut buf) { - Ok(read_bytes) => { - if let Some(logger) = &mut self.logger { - io_logger_sealed::Sealed::log_output( - logger.as_mut(), - &buf[..read_bytes], - ); - } - sink.write_all(&buf[..read_bytes])?; + let mut buf = [0u8; BUFFER_LEN]; + loop { + match source.read(&mut buf) { + Ok(read_bytes) => { + if read_bytes == 0 { + break; + } + if let Some(logger) = &mut self.logger { + io_logger_sealed::Sealed::log_output(logger.as_mut(), &buf[..read_bytes]); } - Err(e) if e.kind() == io::ErrorKind::WouldBlock => break, - Err(e) => return Err(e), + sink.write_all(&buf[..read_bytes])?; } + Err(e) if e.kind() == io::ErrorKind::WouldBlock => break, + Err(e) => return Err(e), } } sink.flush() diff --git a/rar-exec/src/runner.rs b/rar-exec/src/runner.rs index 59678f94..2a68acae 100644 --- a/rar-exec/src/runner.rs +++ b/rar-exec/src/runner.rs @@ -1,8 +1,8 @@ use std::io; use std::os::unix::process::{CommandExt, ExitStatusExt}; -use std::process::Command; +use std::process::{Command, Stdio}; -use log::error; +use log::{debug, error, trace, warn}; use crate::monitor::backchannel::{ Backchannel, MonitorMessage, ParentMessage, RUNNER_SIGNALS_NO_PTY, RUNNER_SIGNALS_WITH_PTY, @@ -14,7 +14,7 @@ use super::event::{EventRegistry, PollEvent, Process, StopReason}; use super::pipe::{IoLogger, Pipe, io_logger_sealed}; use super::pty::{Pty, PtyLeader}; use super::signal::{SignalStream, register_signal_handler}; -use super::terminal::UserTerm; +use super::terminal::{ProcessId, TermSize, TerminalExt, UserTerm}; pub struct SimpleFileLogger { file: std::fs::File, @@ -72,6 +72,11 @@ pub struct ExecRunner { monitor_pid: i32, backchannel: Backchannel, command_pid: i32, + parent_pgrp: ProcessId, + tty_size: TermSize, + foreground: bool, + term_raw: bool, + preserve_oflag: bool, } #[derive(Clone, Copy, Debug)] @@ -90,15 +95,18 @@ impl Process for ExecRunner { fn on_event(&mut self, event: Self::Event, registry: &mut EventRegistry) { match event { RunnerEvent::PipeLeft(e) => { + trace!("runner: pipe left event {e:?}"); if let Err(err) = self.pipe.on_left_event(e, registry) { registry.set_break(err); } } RunnerEvent::PipeRight(e) => { + trace!("runner: pipe right event {e:?}"); if let Err(err) = self.pipe.on_right_event(e, registry) { if err.kind() == io::ErrorKind::UnexpectedEof || err.kind() == io::ErrorKind::BrokenPipe { + debug!("runner: pipe right closed, checking monitor exit"); self.check_monitor_exit(registry); } else { registry.set_break(err); @@ -106,89 +114,172 @@ impl Process for ExecRunner { } } RunnerEvent::Signal => { + trace!("runner: signal event"); // Consume all pending signals loop { match self.signal_stream.recv() { - Ok(info) => self.handle_signal(info.signal, registry), + Ok(info) => self.handle_signal(info, registry), Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break, Err(e) => { + warn!("runner: signal recv error: {e}"); registry.set_break(e); break; } } } } - RunnerEvent::Backchannel => { - loop { - match self.backchannel.recv_parent_message() { - Ok(msg) => self.handle_monitor_message(msg, registry), - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break, - Err(e) => { - // If monitor closes connection, it usually means it exited. - if e.kind() == io::ErrorKind::UnexpectedEof - || e.kind() == std::io::ErrorKind::ConnectionReset - { - self.check_monitor_exit(registry); - break; - } - registry.set_break(e); - break; - } - } - } - } + RunnerEvent::Backchannel => self.on_backchannel_readable(registry), } } } impl ExecRunner { - fn handle_signal(&mut self, signal: libc::c_int, registry: &mut EventRegistry) { - match signal { + fn handle_signal( + &mut self, + info: crate::signal::SignalInfo, + registry: &mut EventRegistry, + ) { + debug!("runner: got signal {} from pid {}", info.signal, info.pid); + match info.signal { libc::SIGCHLD => { // Potentially monitor exited self.check_monitor_exit(registry); } + libc::SIGCONT => { + debug!("runner: SIGCONT -> resume terminal"); + let _ = self.resume_terminal(); + } libc::SIGWINCH => { // Propagate resize - if let Ok(size) = self.pipe.left().get_size() { - let _ = self.pipe.right().set_size(&size); - } + debug!("runner: SIGWINCH -> handle resize"); + let _ = self.handle_sigwinch(); } _ => { // Forward signal to monitor process via backchannel - if is_forward_signal_allowed(signal) + if is_forward_signal_allowed(info.signal) + && !self.is_self_terminating(info.pid) && self .backchannel - .send_monitor_message(&MonitorMessage::Signal(signal)) + .send_monitor_message(&MonitorMessage::Signal(info.signal)) .is_err() { // If send fails, monitor is likely gone + warn!( + "runner: failed to forward signal {}, checking monitor exit", + info.signal + ); self.check_monitor_exit(registry); } } } } - fn handle_monitor_message(&mut self, msg: ParentMessage, registry: &mut EventRegistry) { - match msg { - ParentMessage::CommandPid(pid) => { - self.command_pid = pid; - } - ParentMessage::ExitStatus(status) => { - registry.set_exit(std::process::ExitStatus::from_raw(status)); + fn on_backchannel_readable(&mut self, registry: &mut EventRegistry) { + trace!("runner: backchannel readable"); + loop { + match self.backchannel.recv_parent_message() { + Ok(ParentMessage::CommandPid(pid)) => { + debug!("runner: command pid set to {pid}"); + self.command_pid = pid; + } + Ok(ParentMessage::ExitStatus(status)) => { + debug!("runner: exit status received: {status}"); + registry.set_exit(std::process::ExitStatus::from_raw(status)); + break; + } + Ok(ParentMessage::Error(err)) => { + warn!("runner: error from monitor: {err}"); + registry.set_break(io::Error::other(err)); + break; + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break, + Err(e) => { + // If monitor closes connection, it usually means it exited. + if e.kind() == io::ErrorKind::UnexpectedEof + || e.kind() == std::io::ErrorKind::ConnectionReset + { + warn!("runner: backchannel closed, checking monitor exit"); + self.check_monitor_exit(registry); + break; + } + warn!("runner: backchannel recv error: {e}"); + registry.set_break(e); + break; + } } - ParentMessage::Error(err) => { - registry.set_break(io::Error::other(err)); + } + } + + fn handle_sigwinch(&mut self) -> io::Result<()> { + let new_size = self.pipe.left().get_size()?; + if new_size != self.tty_size { + debug!("runner: resize {} -> {}", self.tty_size, new_size); + self.pipe.right().set_size(&new_size)?; + self.tty_size = new_size; + + if self.command_pid > 0 { + debug!("runner: send SIGWINCH to pgid {}", self.command_pid); + unsafe { libc::killpg(self.command_pid, libc::SIGWINCH) }; } } + Ok(()) + } + + fn resume_terminal(&mut self) -> io::Result<()> { + if self.term_raw && self.foreground { + debug!( + "runner: restoring raw mode (preserve_oflag={})", + self.preserve_oflag + ); + self.pipe + .left_mut() + .set_raw_mode(true, self.preserve_oflag)?; + } + Ok(()) + } + + fn is_self_terminating(&self, signaler_pid: libc::pid_t) -> bool { + if signaler_pid <= 0 || self.command_pid <= 0 { + return false; + } + + if signaler_pid == self.command_pid { + return true; + } + + let signaler_pgrp = unsafe { libc::getpgid(signaler_pid) }; + signaler_pgrp == self.command_pid } fn check_monitor_exit(&mut self, registry: &mut EventRegistry) { let mut status = 0; let res = unsafe { libc::waitpid(self.monitor_pid, &raw mut status, libc::WNOHANG) }; if res > 0 { - registry.set_exit(std::process::ExitStatus::from_raw(status)); + debug!("runner: monitor exited with status {status}"); + + // Critical: Restore terminal settings and foreground pgrp to prevent freeze on next invocation + if self.term_raw { + if let Err(e) = self.pipe.left_mut().restore(false) { + warn!("runner: failed to restore terminal settings: {e}"); + } else { + debug!("runner: terminal settings restored"); + } + } + + // Restore foreground process group to parent + if self.foreground { + if let Err(e) = self.pipe.left_mut().tcsetpgrp_nobg(self.parent_pgrp) { + warn!("runner: failed to restore foreground pgrp: {e}"); + } else { + debug!( + "runner: restored foreground pgrp to parent ({})", + self.parent_pgrp.inner() + ); + } + } + let _ = self.pipe.flush_left(); + registry.set_exit(std::process::ExitStatus::from_raw(status)); } } } @@ -205,19 +296,32 @@ pub fn run_no_pty( mut command: Command, orchestrator: Orchestrator, ) -> io::Result { + let original_set = block_all_signals(); + // initialize signals BEFORE spawning child to minimize race window let signal_stream = SignalStream::init()?; for &sig in RUNNER_SIGNALS_NO_PTY { register_signal_handler(sig)?; } + debug!("runner(no_pty): signal handlers installed"); // orchestration: set up pre_exec hooks before spawning + // Also restore the signal mask in the child so forwarded signals are delivered. + let child_sigset = original_set; unsafe { let ctx = PreExecContext::empty(); - command.pre_exec(move || orchestrator.run(&ctx)); + command.pre_exec(move || { + restore_signals(child_sigset); + orchestrator.run(&ctx) + }); } let mut child = command.spawn()?; + let child_pid = + i32::try_from(child.id()).map_err(|_| io::Error::other("child pid out of range"))?; + debug!("runner(no_pty): child pid {child_pid}"); + + restore_signals(original_set); // event loop for signals loop { @@ -233,10 +337,19 @@ pub fn run_no_pty( return Ok(status); } } + libc::SIGALRM => { + unsafe { libc::kill(child_pid, libc::SIGKILL) }; + } + libc::SIGWINCH => { + unsafe { libc::kill(child_pid, libc::SIGWINCH) }; + } _ => { + if info.pid > 0 && info.pid == child_pid { + continue; + } // Forward signal if is_forward_signal_allowed(info.signal) { - unsafe { libc::kill(child.id().cast_signed(), info.signal) }; + unsafe { libc::kill(child_pid, info.signal) }; } } } @@ -266,6 +379,20 @@ pub fn run( } } +/// This function manages command execution with a Pty, and TWO forks (so 3 processes): +/// +/// 1. Parent - monitor signals and I/O +/// 2. Monitor - forward signals, detect issues (command exits) +/// 3. Command - the executed command +/// +/// (Parent --> monitor --> Command) +/// +/// This allows to : +/// - Better signal management +/// - Better error management +/// - Better termination process +/// - Better TTY management. +/// /// # Errors /// Returns an error if the execution fails due to : /// - Failure to fork process @@ -274,34 +401,98 @@ pub fn run( /// - Failure in the backchannel communication /// - Failure in the orchestrator's pre-exec function /// - Failure in spawning the command execution +#[allow(clippy::too_many_lines)] pub fn run_with_pty( command: Command, orchestrator: Orchestrator, logger: Option>, mut user_term: UserTerm, ) -> io::Result { + let original_set = block_all_signals(); + + let parent_pgrp = ProcessId::new(unsafe { libc::getpgrp() }); + let mut foreground = user_term + .as_tty() + .ok() + .and_then(|tty| tty.tcgetpgrp().ok()) + .is_some_and(|tty_pgrp| tty_pgrp == parent_pgrp); + + let mut exec_bg = false; + let mut preserve_oflag = false; + let mut term_raw = false; + // initialize signals // SIGTTIN and SIGTTOU are ignored to prevent the runner from being suspended // when interacting with the terminal in background - let signal_stream = SignalStream::init()?; - for &sig in RUNNER_SIGNALS_WITH_PTY { - register_signal_handler(sig)?; + // NOTE: SignalStream initialization is done after fork to match sudo-rs behavior. + + // Create Pty + let pty = Pty::open()?; + let tty_size = user_term.get_size().unwrap_or_else(|_| TermSize::new(0, 0)); + pty.leader.set_size(&tty_size)?; // set window size by default, resizing comes with features + debug!("runner(pty): initial tty size {tty_size}"); + + let mut command = command; + let mut stdin_set = false; + let mut stdout_set = false; + let mut stderr_set = false; + + if !std::io::stdin().is_terminal_for_pgrp(parent_pgrp) { + debug!("runner(pty): stdin not a terminal for parent pgrp"); + if std::io::stdin().is_pipe_or_socket() { + exec_bg = true; + } + command.stdin(Stdio::inherit()); + stdin_set = true; } - unsafe { - libc::signal(libc::SIGTTIN, libc::SIG_IGN); - libc::signal(libc::SIGTTOU, libc::SIG_IGN); + + if !std::io::stdout().is_terminal_for_pgrp(parent_pgrp) { + debug!("runner(pty): stdout not a terminal for parent pgrp"); + if std::io::stdout().is_pipe_or_socket() { + exec_bg = true; + preserve_oflag = true; + } + command.stdout(Stdio::inherit()); + stdout_set = true; } - user_term.set_raw_mode(true, true)?; + if !std::io::stderr().is_terminal_for_pgrp(parent_pgrp) { + debug!("runner(pty): stderr not a terminal for parent pgrp"); + command.stderr(Stdio::inherit()); + stderr_set = true; + } - let pty = Pty::open()?; - if let Ok(sz) = user_term.get_size() { - pty.leader.set_size(&sz)?; + if std::io::stdout().is_pipe_or_socket() { + debug!("runner(pty): stdout is pipe/socket -> background"); + foreground = false; } + if !stdin_set { + command.stdin(Stdio::from(pty.follower.try_clone()?)); + } + if !stdout_set { + command.stdout(Stdio::from(pty.follower.try_clone()?)); + } + if !stderr_set { + command.stderr(Stdio::from(pty.follower.try_clone()?)); + } + + if foreground { + // Preserve oflag for interactive terminals (keeps ONLCR for proper CR/LF mapping) + let preserve = !exec_bg || preserve_oflag; + debug!("runner(pty): enabling raw mode preserve_oflag={preserve}"); + user_term.set_raw_mode(true, preserve)?; + term_raw = true; + } + + // Copy terminal settings from user terminal to PTY follower to ensure interactive I/O works + user_term.copy_to(&pty.follower)?; + // Create backchannel let (mut parent_channel, monitor_channel) = Backchannel::pair()?; + debug!("runner(pty): backchannel created"); + // TODO: Verify if there isn't a better API for forking // FORK: Separate Parent and Monitor // SAFETY: Single threaded at this point let pid = unsafe { libc::fork() }; @@ -322,28 +513,38 @@ pub fn run_with_pty( command, orchestrator, monitor_channel, - None, + original_set.as_ref(), + foreground && !exec_bg, ); if let Err(e) = res { error!("Monitor failed: {e}"); unsafe { libc::_exit(1) }; } - // SAFETY: exec_monitor_process should have replaced the process image via exec(). - // If we reach this point, exec() failed and we already exited above with code 1. - // This line is unreachable in normal execution. - unreachable!("exec_monitor_process must either exec or exit"); + unsafe { libc::exit(0) }; } // --- PARENT PROCESS --- + let signal_stream = SignalStream::init()?; + for &sig in RUNNER_SIGNALS_WITH_PTY { + register_signal_handler(sig)?; + } + debug!("runner(pty): signal handlers installed"); + // we ignore SIGTTIN and SIGTOU, no suspend here. + unsafe { + libc::signal(libc::SIGTTIN, libc::SIG_IGN); + libc::signal(libc::SIGTTOU, libc::SIG_IGN); + } + drop(command); drop(pty.follower); // Parent doesn't need follower drop(monitor_channel); // Parent doesn't need monitor channel pty.leader.set_nonblocking()?; + debug!("runner(pty): pty leader set nonblocking"); let mut registry = EventRegistry::new(); - let pipe = Pipe::new( + let mut pipe = Pipe::new( user_term, pty.leader, &mut registry, @@ -352,6 +553,11 @@ pub fn run_with_pty( logger, ); + if !foreground || exec_bg { + debug!("runner(pty): disabling input (foreground={foreground}, exec_bg={exec_bg})"); + pipe.disable_input(&mut registry); + } + // signals registry.register_event(signal_stream, PollEvent::Readable, |_| RunnerEvent::Signal); registry.register_event(parent_channel.get_mut(), PollEvent::Readable, |_| { @@ -364,6 +570,11 @@ pub fn run_with_pty( monitor_pid: pid, // Parent tracks monitor backchannel: parent_channel, command_pid: 0, + parent_pgrp, + tty_size, + foreground, + term_raw, + preserve_oflag, }; // HANDSHAKE 1: Send "Start" (Edge) to monitor to let it spawn the command @@ -374,6 +585,12 @@ pub fn run_with_pty( { return Err(io::Error::other(format!("Failed to start monitor: {e}"))); } + debug!("runner(pty): sent start edge to monitor"); + + runner.backchannel.set_nonblocking(true)?; + debug!("runner(pty): backchannel set nonblocking"); + + restore_signals(original_set); let res = registry.event_loop(&mut runner); @@ -384,9 +601,19 @@ pub fn run_with_pty( let _ = runner .backchannel .send_monitor_message(&MonitorMessage::Edge); + debug!("runner(pty): sent stop edge to monitor"); // restore - runner.pipe.left_mut().restore(true)?; + if runner.term_raw + && runner + .pipe + .left() + .as_tty() + .and_then(|tty| tty.tcgetpgrp()) + .is_ok_and(|pgrp| pgrp == runner.parent_pgrp) + { + runner.pipe.left_mut().restore(true)?; + } match res { StopReason::Exit(status) => { @@ -397,6 +624,25 @@ pub fn run_with_pty( } } +fn block_all_signals() -> Option { + let mut set = unsafe { std::mem::zeroed::() }; + unsafe { + libc::sigfillset(&raw mut set); + } + + let mut old = unsafe { std::mem::zeroed::() }; + let res = unsafe { libc::sigprocmask(libc::SIG_BLOCK, &raw const set, &raw mut old) }; + if res == -1 { None } else { Some(old) } +} + +fn restore_signals(original: Option) { + if let Some(set) = original { + unsafe { + libc::sigprocmask(libc::SIG_SETMASK, &raw const set, std::ptr::null_mut()); + } + } +} + #[cfg(test)] mod tests { use super::{SimpleFileLogger, run_no_pty}; @@ -456,6 +702,7 @@ mod tests { )) } + #[serial] #[test] fn simple_file_logger_writes_expected_prefixes() { let path = unique_log_path("prefixes"); @@ -473,6 +720,7 @@ mod tests { let _ = fs::remove_file(path); } + #[serial] #[test] fn simple_file_logger_appends_across_instances() { let path = unique_log_path("append"); @@ -498,6 +746,7 @@ mod tests { let _ = fs::remove_file(path); } + #[serial] #[test] fn simple_file_logger_supports_empty_payloads() { let path = unique_log_path("empty"); @@ -515,6 +764,7 @@ mod tests { let _ = fs::remove_file(path); } + #[serial] #[test] fn run_no_pty_returns_command_exit_code() { let mut command = Command::new("sh"); @@ -547,8 +797,21 @@ mod tests { #[serial] #[test] fn run_no_pty_forwards_signals_to_child() { - let sender = thread::spawn(|| { - thread::sleep(std::time::Duration::from_millis(250)); + let ready_path = std::env::temp_dir().join(format!( + "rar_exec_runner_ready_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time before UNIX_EPOCH") + .as_nanos() + )); + let ready_path_for_sender = ready_path.clone(); + + let sender = thread::spawn(move || { + while !ready_path_for_sender.exists() { + thread::sleep(std::time::Duration::from_millis(10)); + } + thread::sleep(std::time::Duration::from_millis(50)); unsafe { libc::kill(libc::getpid(), libc::SIGHUP); } @@ -556,12 +819,49 @@ mod tests { let mut command = Command::new("sh"); command + .env("READY_FILE", &ready_path) .arg("-c") - .arg("trap 'exit 42' HUP; while :; do sleep 1; done"); + .arg("touch \"$READY_FILE\"; trap 'exit 42' HUP; while :; do sleep 1; done"); let status = run_no_pty(command, Orchestrator::new(NOOP_STEPS)).expect("run command"); sender.join().expect("signal sender thread joined"); + let _ = std::fs::remove_file(&ready_path); assert_eq!(status.code(), Some(42)); } + + #[test] + fn block_and_restore_signals_roundtrip() { + fn current_mask() -> libc::sigset_t { + let mut set = unsafe { std::mem::zeroed::() }; + unsafe { + libc::sigprocmask(libc::SIG_SETMASK, std::ptr::null(), &raw mut set); + } + set + } + + let original = current_mask(); + let saved = super::block_all_signals(); + assert!( + saved.is_some(), + "block_all_signals must return previous set" + ); + + let blocked = current_mask(); + let sigint_blocked = + unsafe { libc::sigismember(std::ptr::from_ref(&blocked), libc::SIGINT) }; + assert_eq!(sigint_blocked, 1, "SIGINT should be blocked"); + + super::restore_signals(saved); + let restored = current_mask(); + + let same = unsafe { + libc::memcmp( + std::ptr::from_ref(&original).cast::(), + std::ptr::from_ref(&restored).cast::(), + std::mem::size_of::(), + ) == 0 + }; + assert!(same, "signal mask should be restored"); + } } diff --git a/rar-exec/src/signal.rs b/rar-exec/src/signal.rs index 2201791b..a1027b95 100644 --- a/rar-exec/src/signal.rs +++ b/rar-exec/src/signal.rs @@ -67,7 +67,7 @@ impl PipeFactory { } #[repr(C)] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub struct SignalInfo { pub signal: SignalNumber, pub pid: libc::pid_t, @@ -76,17 +76,7 @@ pub struct SignalInfo { } impl SignalInfo { - pub const SIZE: usize = std::mem::size_of::(); - - #[must_use] - pub const fn new(signal: SignalNumber) -> Self { - Self { - signal, - pid: 0, - uid: 0, - status: 0, - } - } + pub const SIZE: usize = std::mem::size_of::(); } pub struct SignalStream { @@ -134,19 +124,24 @@ impl SignalStream { /// # Errors /// Returns an error if reading from the signal stream fails pub fn recv(&self) -> io::Result { - let mut signal: SignalNumber = 0; + let mut info = SignalInfo { + signal: 0, + pid: 0, + uid: 0, + status: 0, + }; loop { // SAFETY: valid pointer and size to read one signal number. let n = unsafe { libc::read( self.rx.as_raw_fd(), - (&raw mut signal).cast(), + (&raw mut info).cast(), SignalInfo::SIZE, ) }; if n == SignalInfo::SIZE.cast_signed() { - return Ok(SignalInfo::new(signal)); + return Ok(info); } else if n == 0 { return Err(io::Error::from(io::ErrorKind::UnexpectedEof)); } else if n == -1 { @@ -177,14 +172,28 @@ impl AsFd for SignalStream { // - Multiple signals arrive faster than they can be consumed by event loop // - Event loop is blocked processing other events // - The receiver (SignalStream::recv) isn't called frequently enough -extern "C" fn handler(signal: SignalNumber) { +extern "C" fn handler(signal: SignalNumber, info: *mut libc::siginfo_t, _ctx: *mut libc::c_void) { // Use Acquire ordering to ensure we see the most recent value written by init() let fd = WRITE_FD.load(Ordering::Acquire); if fd < 0 { return; } - let value = signal; + let (pid, uid, status) = if info.is_null() { + (0, 0, 0) + } else { + // SAFETY: info is provided by the kernel for SA_SIGINFO handlers. + let info = unsafe { &*info }; + // SAFETY: accessors rely on the kernel-populated siginfo_t. + unsafe { (info.si_pid(), info.si_uid(), info.si_status()) } + }; + + let value = SignalInfo { + signal, + pid, + uid, + status, + }; // SAFETY: `write(2)` is async-signal-safe and the pointer is valid for `SignalInfo::SIZE` bytes. // Note to myself in the future: Errors (including EAGAIN if buffer full) are silently ignored. // This is acceptable since the kernel queues pending signals and re-notifies when the queue is consumed. @@ -200,8 +209,8 @@ extern "C" fn handler(signal: SignalNumber) { pub fn register_signal_handler(signal: SignalNumber) -> io::Result<()> { let mut sa: libc::sigaction = unsafe { std::mem::zeroed() }; sa.sa_sigaction = handler as *const () as usize; - // Use SA_RESTART to avoid interrupting system calls - sa.sa_flags = libc::SA_RESTART; + // Use SA_RESTART to avoid interrupting system calls and SA_SIGINFO for metadata. + sa.sa_flags = libc::SA_RESTART | libc::SA_SIGINFO; // Block all signals during signal handler execution to prevent re-entrancy issues // SAFETY: valid sigset pointer. @@ -229,7 +238,9 @@ mod tests { let stream = SignalStream::init().expect("Failed to init SignalStream"); signal::register_signal_handler(libc::SIGUSR1).expect("Failed to register SIGUSR1"); - super::handler(libc::SIGUSR1); + unsafe { + libc::raise(libc::SIGUSR1); + } let mut found = false; for _ in 0..50 { @@ -238,7 +249,6 @@ mod tests { // Verify we received the correct data if info.signal == libc::SIGUSR1 { found = true; - assert_eq!(info.pid, 0); break; } } @@ -250,7 +260,9 @@ mod tests { // Verification that we can handle multiple different signals signal::register_signal_handler(libc::SIGUSR2).expect("Failed to register SIGTRAP"); - super::handler(libc::SIGUSR2); + unsafe { + libc::raise(libc::SIGUSR2); + } found = false; for _ in 0..50 { diff --git a/rar-exec/src/terminal.rs b/rar-exec/src/terminal.rs index 6942ede4..ed93da52 100644 --- a/rar-exec/src/terminal.rs +++ b/rar-exec/src/terminal.rs @@ -399,13 +399,15 @@ impl UserTerm { }; let original_oflag = term.c_oflag; unsafe { cfmakeraw(&raw mut term) }; + term.c_iflag |= libc::ICRNL | libc::IXON; + term.c_oflag |= libc::OPOST | libc::ONLCR; if preserve_oflag { term.c_oflag = original_oflag; } else { term.c_oflag = 0; } if with_signals { - term.c_cflag |= ISIG; + term.c_lflag |= ISIG; } unsafe { tcsetattr_nobg(fd, TCSADRAIN, &raw const term) }?; @@ -462,8 +464,10 @@ impl Drop for UserTerm { mod tests { use log::warn; + use crate::pty::Pty; use crate::terminal::{self, ProcessId, TermSize, TerminalExt, UserTerm}; use std::fs::File; + use std::os::unix::ffi::OsStrExt; #[test] fn test_terminal_trait_on_file() { @@ -532,4 +536,14 @@ mod tests { let (s1, _s2) = UnixStream::pair().expect("Failed to create socket pair"); assert!(s1.is_pipe_or_socket()); } + + #[test] + fn test_ttyname_matches_pty_path() { + let pty = Pty::open().expect("Failed to open PTY"); + let ttyname = pty + .follower + .ttyname() + .expect("ttyname should work on pty follower"); + assert_eq!(ttyname.as_os_str().as_bytes(), pty.path.as_bytes()); + } } diff --git a/resources/opensuse/opensuse.conf b/resources/opensuse/opensuse.conf new file mode 100644 index 00000000..b5ebcb11 --- /dev/null +++ b/resources/opensuse/opensuse.conf @@ -0,0 +1,6 @@ +#%PAM-1.0 +auth include common-auth +account include common-account +password include common-password +session optional pam_keyinit.so revoke +session include common-session-nonlogin \ No newline at end of file diff --git a/resources/rootasrole.json b/resources/rootasrole.json index b1160080..ce09c89a 100644 --- a/resources/rootasrole.json +++ b/resources/rootasrole.json @@ -1,5 +1,5 @@ { - "version": "3.3.0", + "version": "4.0.0", "storage": { "method": "json", "settings": { @@ -40,7 +40,8 @@ "PS2", "XAUTHORY", "XAUTHORIZATION", - "XDG_CURRENT_DESKTOP" + "XDG_CURRENT_DESKTOP", + "RUST_LOG" ], "check": [ "COLORTERM", @@ -58,14 +59,16 @@ "PERL5LIB", "PERL5OPT", "PYTHONINSPECT" - ], - "set": {} + ] }, "authentication": "perform", "root": "user", "bounding": "strict", "umask": "022", - "execinfo": "hide" + "execinfo": "hide", + "workdir": { + "default": "all" + } }, "roles": [ { @@ -82,6 +85,14 @@ ], "tasks": [ { + "options": { + "workdir": { + "default": "all" + }, + "env": { + "override_behavior": true + } + }, "name": "t_root", "purpose": "access to every commands", "cred": { @@ -95,17 +106,10 @@ }, "capabilities": { "default": "all", - "sub": [ - "CAP_LINUX_IMMUTABLE" - ] + "sub": ["CAP_LINUX_IMMUTABLE"] } }, - "commands": "all", - "options": { - "env": { - "override_behavior": true - } - } + "commands": "all" }, { "name": "t_chsr", @@ -113,15 +117,11 @@ "cred": { "setuid": "root", "setgid": "root", - "capabilities": [ - "CAP_LINUX_IMMUTABLE" - ] + "capabilities": ["CAP_LINUX_IMMUTABLE"] }, - "commands": [ - "/usr/bin/chsr ^.*$" - ] + "commands": ["/usr/bin/chsr ^.*$"] } ] } ] -} \ No newline at end of file +} diff --git a/src/chsr/cli/cli.pest b/src/chsr/cli/cli.pest index 1ddb42db..8f25d305 100644 --- a/src/chsr/cli/cli.pest +++ b/src/chsr/cli/cli.pest @@ -1,5 +1,5 @@ cli = { SOI ~ args ~ EOI } -args = { help | convert_op | list | role | editor | options_operations } +args = { help | convert_op | list | file | role | editor_op | options_operations } list = { ("show" | "list" | "l") } set = { "set" | "s" } @@ -20,6 +20,13 @@ all = { "all" } name = @{ (!WHITESPACE ~ ANY)+ } +// ======================== +// editor +// ======================== +editor_op = { editor ~ editor_type? ~ editor_path? } +editor_type = { "--type" ~ convert_type } +editor_path = @{ name } + // ======================== // convert @@ -31,6 +38,16 @@ to = { convert_type ~ path } convert_type = { "json" | "cbor" } convert_reconfigure = { "--reconfigure" | "-r" } +// ======================== +// file +// ======================== + +// chsr file /path/to/file role r1 create +// chsr f /path/to/file show (all|role|options) + +file = { ("file" | "f") ~ (list | file_ops) } +file_ops = { policy_path ~ (list | role | options_operations) } +policy_path = @{ name } // ======================== // role @@ -128,7 +145,7 @@ caps_listing = { (whitelist | blacklist) ~ ((add | del | set) ~ capabilities // options // ======================== -// chsr (r r1) (t t1) options show (all|path|env|root|bounding) +// chsr (f /foo/bar) (r r1) (t t1) options show (all|path|env|root|bounding) // 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 @@ -145,7 +162,7 @@ caps_listing = { (whitelist | blacklist) ~ ((add | del | set) ~ capabilities // chsr o t unset --type --duration --max_usage options_operations = { ("options" | "o") ~ opt_args } -opt_args = _{ opt_show | opt_path | opt_env | opt_root | opt_bounding | opt_timeout | opt_skip_auth | opt_execinfo | opt_mask } +opt_args = _{ opt_show | opt_path | opt_workdir | opt_env | opt_root | opt_bounding | opt_timeout | opt_skip_auth | opt_execinfo | opt_mask } opt_show = _{ list ~ opt_show_arg? } opt_show_arg = { "all" | "cmd" | "cred" | "path" | "env" | "root" | "bounding" | "timeout" } @@ -158,6 +175,11 @@ path_policy = { "delete-all" | "keep-safe" | "keep-unsafe" | "inherit" } opt_path_listing = { (whitelist | blacklist) ~ (((add | del | set) ~ path) | purge) } path = @{ name } +opt_workdir = { ("workdir" | "w") ~ opt_workdir_args } +opt_workdir_args = _{ (setpolicy ~ workdir_policy) | (opt_workdir_listing) | (set ~ path) } +opt_workdir_listing = { (whitelist | blacklist) ~ (((add | del | set) ~ path) | purge) } +workdir_policy = { "all" | "none" | "inherit" } + opt_env = { "env" ~ (opt_env_args | help) } opt_env_args = _{ opt_env_setpolicy | opt_env_keep | opt_env_delete | opt_env_set | opt_env_listing | opt_env_setlisting } opt_env_setpolicy = { setpolicy ~ env_policy } diff --git a/src/chsr/cli/data.rs b/src/chsr/cli/data.rs index aba5c1a1..efb44710 100644 --- a/src/chsr/cli/data.rs +++ b/src/chsr/cli/data.rs @@ -7,15 +7,15 @@ use indexmap::IndexSet; use pest_derive::Parser; use rar_common::{ - StorageMethod, database::{ actor::{SActor, SGroups, SUserType}, options::{ EnvBehavior, EnvKey, OptType, PathBehavior, SAuthentication, SBounding, SInfo, - SPrivileged, SUMask, TimestampType, + SPrivileged, SUMask, TimestampType, WorkdirBehavior, }, structs::{IdTask, SetBehavior}, }, + util::StorageMethod, }; #[derive(Parser)] @@ -65,15 +65,20 @@ pub enum TimeoutOpt { MaxUsage, } +#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Default)] pub struct Inputs { pub action: InputAction, pub editor: bool, + pub editor_path: Option, + pub editor_type: Option, pub setlist_type: Option, pub timeout_arg: Option<[bool; 3]>, pub timeout_type: Option, pub timeout_duration: Option, pub timeout_max_usage: Option, + pub policy: bool, + pub policy_path: Option, pub role_id: Option, pub role_type: Option, pub actors: Option>, @@ -97,6 +102,7 @@ pub struct Inputs { pub options_auth: Option, pub options_execinfo: Option, pub options_umask: Option, + pub options_workdir_policy: Option, pub convertion: Option, pub convert_reconfigure: bool, } diff --git a/src/chsr/cli/editor.rs b/src/chsr/cli/editor.rs index 6053e124..c9a47e7b 100644 --- a/src/chsr/cli/editor.rs +++ b/src/chsr/cli/editor.rs @@ -1,23 +1,19 @@ use std::{ - cell::RefCell, error::Error, + fmt::Debug, io::{BufRead, Seek, Write}, + os::unix::process::CommandExt, path::Path, - rc::Rc, }; use log::{debug, warn}; -use rar_common::{ - FullSettings, - database::{ - actor::{SActor, SGroups}, - structs::{SCommands, SCredentials, SGroupsEither, SRole, STask}, - versionning::Versioning, - }, - migrate_settings, -}; +use rar_common::database::warn::Warn; +use serde::{Serialize, de::DeserializeOwned}; +use std::os::unix::fs::PermissionsExt; use std::{fs::File, io::stdin, process::Command}; +use crate::security::seccomp_lock; + pub struct Defer(Option); impl Defer { @@ -38,418 +34,81 @@ pub const fn defer(f: F) -> Defer { Defer::new(f) } -fn warn_anomalies(full_settings: &Versioning, mut warn: F) -where - F: FnMut(String), -{ - let config = &full_settings.data.config; - if let Some(config) = config { - for key in config.as_ref().borrow().extra_fields.keys() { - warn(format!("Warning: Unknown configuration field '{key}'")); - } - if let Some(opt) = &config.as_ref().borrow().options { - for key in opt.as_ref().borrow().extra_fields.keys() { - warn(format!( - "Warning: Unknown options field at {:?} level '{}'", - opt.as_ref().borrow().level, - key - )); - } - } - for role in &config.as_ref().borrow().roles { - for key in role.as_ref().borrow().extra_fields.keys() { - warn(format!( - "Warning: Unknown role field in role '{}' : '{}'", - role.as_ref().borrow().name, - key - )); - } - warn_actors(role, &mut warn); - if let Some(opt) = &role.as_ref().borrow().options { - for key in opt.as_ref().borrow().extra_fields.keys() { - warn(format!( - "Warning: Unknown options field at {:?} level in role '{}' : '{}'", - opt.as_ref().borrow().level, - role.as_ref().borrow().name, - key - )); - } - } - for task in &role.as_ref().borrow().tasks { - for key in task.as_ref().borrow().extra_fields.keys() { - warn(format!( - "Warning: Unknown task field in role '{}' task '{:?}' : '{}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - key - )); - } - warn_cred(role, task, &task.as_ref().borrow().cred, &mut warn); - warn_cmds(role, task, &task.as_ref().borrow().commands, &mut warn); - if let Some(opt) = &task.as_ref().borrow().options { - for key in opt.as_ref().borrow().extra_fields.keys() { - warn(format!( - "Warning: Unknown options field at {:?} level in role '{}' task '{:?}' : '{}'", - opt.as_ref().borrow().level, - role.as_ref().borrow().name, - task.as_ref().borrow().name, - key - )); - } - } - } - } - } else { - warn("Warning: No configuration section found in settings.".to_string()); - } -} +pub const SYSTEM_EDITOR_LIST: &[&str] = &konst::iter::collect_const!(&str => + konst::string::split(env!("RAR_CHSR_EDITOR_PATH"), ","), + map(str::trim_ascii), +); -fn warn_cmds( - role: &Rc>, - task: &Rc>, - cmds: &SCommands, - warn: &mut F, -) where - F: FnMut(String), -{ - cmds.extra_fields.keys().for_each(|key| { - warn(format!( - "Warning: Unknown commands field in role '{}' task '{:?}' : '{}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - key - )); - }); - if cmds.add.is_empty() - && !cmds - .default - .as_ref() - .is_some_and(|b| *b == rar_common::database::structs::SetBehavior::All) - { - warn(format!( - "Warning: No commands can be performed in role '{}' task '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name - )); - } - for cmd in &cmds.add { - match cmd { - rar_common::database::structs::SCommand::Simple(cmd) => { - if cmd.is_empty() { - warn(format!( - "Warning: Empty command in role '{}' task '{:?}' in add list", - role.as_ref().borrow().name, - task.as_ref().borrow().name - )); - } - } - rar_common::database::structs::SCommand::Complex(value) => { - if value.as_object().is_none() { - warn(format!( - "Warning: Complex command is not an dictionnary in role '{}' task '{:?}' : '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - value - )); - } - } - } - } - for cmd in &cmds.sub { - match cmd { - rar_common::database::structs::SCommand::Simple(cmd) => { - if cmd.is_empty() { - warn(format!( - "Warning: Empty command in role '{}' task '{:?}' in sub list", - role.as_ref().borrow().name, - task.as_ref().borrow().name - )); - } - } - rar_common::database::structs::SCommand::Complex(value) => { - if value.as_object().is_none() { - warn(format!( - "Warning: Complex command is not an dictionnary in role '{}' task '{:?}' : '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - value - )); - } - } - } - } +fn is_vim(editor: &str) -> bool { + editor.ends_with("vim") || editor.ends_with("nvim") } -#[allow(clippy::too_many_lines)] -fn warn_cred( - role: &Rc>, - task: &Rc>, - cred: &SCredentials, - warn: &mut F, -) where - F: FnMut(String), -{ - for key in cred.extra_fields.keys() { - warn(format!( - "Warning: Unknown cred field in role '{}' task '{:?}' : '{}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - key - )); +fn is_executable_file(path: &str) -> bool { + if !Path::new(path).is_absolute() { + return false; } - if let Some(id) = &cred.setuid { - match id { - rar_common::database::structs::SUserEither::MandatoryUser(suser_type) => { - if suser_type.fetch_user().is_none() { - warn(format!( - "Warning: Unknown user in role '{}' task '{:?}' setuid: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - suser_type - )); - } - } - rar_common::database::structs::SUserEither::UserSelector(ssetuid_set) => { - if let Some(default) = &ssetuid_set.fallback - && default.fetch_user().is_none() - { - warn(format!( - "Warning: Unknown user in role '{}' task '{:?}' setuid fallback: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - default - )); - } - for add in &ssetuid_set.add { - if add.fetch_user().is_none() { - warn(format!( - "Warning: Unknown user in role '{}' task '{:?}' setuid add: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - add - )); - } - } - for sub in &ssetuid_set.sub { - if sub.fetch_user().is_none() { - warn(format!( - "Warning: Unknown user in role '{}' task '{:?}' setuid sub: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - sub - )); - } - } - } - } - } - if let Some(sgroups_either) = &cred.setgid { - match sgroups_either { - SGroupsEither::MandatoryGroup(group) => { - if group.fetch_group().is_none() { - warn(format!( - "Warning: Unknown group in role '{}' task '{:?}' setgid: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - group - )); - } - } - SGroupsEither::MandatoryGroups(sgroups) => match sgroups { - SGroups::Single(sgroup_type) => { - if sgroup_type.fetch_group().is_none() { - warn(format!( - "Warning: Unknown group in role '{}' task '{:?}' setgid: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - sgroup_type - )); - } - } - SGroups::Multiple(sgroup_types) => { - for sgroup_type in sgroup_types { - if sgroup_type.fetch_group().is_none() { - warn(format!( - "Warning: Unknown group in role '{}' task '{:?}' setgid: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - sgroup_type - )); - } - } - } - }, - SGroupsEither::GroupSelector(chooser) => { - match &chooser.fallback { - SGroups::Single(sgroup_type) => { - if sgroup_type.fetch_group().is_none() { - warn(format!( - "Warning: Unknown group in role '{}' task '{:?}' setgid fallback: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - sgroup_type - )); - } - } - SGroups::Multiple(sgroup_types) => { - for sgroup_type in sgroup_types { - if sgroup_type.fetch_group().is_none() { - warn(format!( - "Warning: Unknown group in role '{}' task '{:?}' setgid fallback: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - sgroup_type - )); - } - } - } - } - chooser.add.iter().for_each(|group| { - match group { - SGroups::Single(sgroup_type) => { - if sgroup_type.fetch_group().is_none() { - warn(format!( - "Warning: Unknown group in role '{}' task '{:?}' setgid add: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - sgroup_type - )); - } - } - SGroups::Multiple(sgroup_types) => { - for sgroup_type in sgroup_types { - if sgroup_type.fetch_group().is_none() { - warn(format!("Warning: Unknown group in role '{}' task '{:?}' setgid add: '{:?}'", role.as_ref().borrow().name, task.as_ref().borrow().name, sgroup_type)); - } - } - } - } - }); - chooser.sub.iter().for_each(|group| { - match group { - SGroups::Single(sgroup_type) => { - if sgroup_type.fetch_group().is_none() { - warn(format!( - "Warning: Unknown group in role '{}' task '{:?}' setgid sub: '{:?}'", - role.as_ref().borrow().name, - task.as_ref().borrow().name, - sgroup_type - )); - } - } - SGroups::Multiple(sgroup_types) => { - for sgroup_type in sgroup_types { - if sgroup_type.fetch_group().is_none() { - warn(format!("Warning: Unknown group in role '{}' task '{:?}' setgid sub: '{:?}'", role.as_ref().borrow().name, task.as_ref().borrow().name, sgroup_type)); - } - } - } - } - }); - } - } + let Ok(meta) = std::fs::metadata(path) else { + return false; + }; + if !meta.is_file() { + return false; } + meta.permissions().mode() & 0o111 != 0 } -fn warn_actors(role: &Rc>, warn: &mut F) +#[cfg_attr(tarpaulin, ignore)] +pub fn start_editing( + folder: &P, + config: &mut T, +) -> Result> where - F: FnMut(String), + P: AsRef, { - for actor in &role.as_ref().borrow().actors { - if actor.is_unknown() { - warn(format!( - "Warning: Unknown actor type in role '{}' : '{:?}'", - role.as_ref().borrow().name, - actor - )); - } else if let SActor::User { id, extra_fields } = actor { - if let Some(id) = id - && id.fetch_user().is_none() - { - warn(format!( - "Warning: Unknown user in role '{}' : '{}'", - role.as_ref().borrow().name, - id - )); - } - for key in extra_fields.keys() { - warn(format!( - "Warning: Unknown user field in role '{}' for user '{:?}' : '{}'", - role.as_ref().borrow().name, - id, - key - )); - } - } else if let SActor::Group { - groups, - extra_fields, - } = actor + let stdin = stdin(); + let mut input = stdin.lock(); + let mut stdout = std::io::stdout(); + // Use RAR_EDITOR only if it is in the build-time whitelist and executable. + let env_editor = std::env::var("RAR_EDITOR").ok(); + let editor = env_editor.map_or_else(String::new, |editor| { + if SYSTEM_EDITOR_LIST.iter().any(|&allowed| allowed == editor) + && is_executable_file(&editor) { - for key in extra_fields.keys() { - warn(format!( - "Warning: Unknown group field in role '{}' for group '{:?}' : '{}'", - role.as_ref().borrow().name, - groups, - key - )); - } - if let Some(groups) = groups { - match groups { - SGroups::Single(sgroup_type) => { - if sgroup_type.fetch_group().is_none() { - warn(format!( - "Warning: Unknown group in role '{}' : '{:?}'", - role.as_ref().borrow().name, - sgroup_type - )); - } - } - SGroups::Multiple(sgroup_types) => { - for sgroup_type in sgroup_types { - if sgroup_type.fetch_group().is_none() { - warn(format!( - "Warning: Unknown group in role '{}' : '{:?}'", - role.as_ref().borrow().name, - sgroup_type - )); - } - } - } - } - } else { - warn(format!( - "Warning: No group specified in role '{}' : '{:?}'", - role.as_ref().borrow().name, - groups - )); - } + debug!("Using editor from RAR_EDITOR env variable: {editor}"); + editor + } else { + warn!("Ignoring RAR_EDITOR: not in whitelist or not executable."); + String::new() } - } -} + }); -pub const SYSTEM_EDITOR: &str = env!("RAR_CHSR_EDITOR_PATH"); + let editor = if editor.is_empty() { + let mut found_editor = None; + for &editor in SYSTEM_EDITOR_LIST { + if is_executable_file(editor) { + found_editor = Some(editor); + break; + } + } + if let Some(editor) = found_editor { + debug!("Using editor from SYSTEM_EDITOR_LIST: {editor}"); + editor.to_string() + } else { + return Err( + "No editor found. Please set RAR_EDITOR to a whitelisted editor path.".into(), + ); + } + } else { + editor + }; -#[cfg_attr(tarpaulin, ignore)] -pub fn edit_config( - folder: &Path, - config: &Rc>, -) -> Result> { - let stdin = stdin(); - let mut input = stdin.lock(); - let mut stdout = std::io::stdout(); - edit_config_internal( - folder, - config, - SYSTEM_EDITOR, - &mut input, - &mut stdout, - |msg| warn!("{msg}"), - ) + edit_internal(folder, config, &editor, &mut input, &mut stdout, |msg| { + warn!("{msg}"); + }) } -fn edit_config_internal( - folder: &Path, - config: &Rc>, +fn edit_internal( + folder: &P, + config: &mut T, editor: &str, input: &mut R, output: &mut W, @@ -459,18 +118,18 @@ where R: BufRead, W: Write, F: FnMut(String), + P: AsRef, { - migrate_settings(&mut config.as_ref().borrow_mut())?; debug!("Using editor: {editor}"); - debug!("Created temporary folder: {}", folder.display()); - let (fd, path) = nix::unistd::mkstemp(&folder.join("config_XXXXXX"))?; + debug!("Created temporary folder: {}", folder.as_ref().display()); + let (fd, path) = nix::unistd::mkstemp(&folder.as_ref().join("config_XXXXXX"))?; debug!("Created temporary file: {}", path.display()); let mut file = File::from(fd); // Write current config to temp file - serde_json::to_writer_pretty(&mut file, &Versioning::new(config.clone()))?; + serde_json::to_writer_pretty(&mut file, &config)?; debug!("Wrote current config to temporary file"); file.flush()?; debug!("Flushed temporary file"); @@ -479,7 +138,7 @@ where loop { let mut cmd = Command::new(editor); - if editor == SYSTEM_EDITOR { + if is_vim(editor) { cmd.arg("-u") .arg("NONE") .arg("-U") @@ -494,9 +153,10 @@ where .arg("set ft=json") .arg("--"); } - + cmd.arg(&path); + debug!("Launching editor: {cmd:?}"); + unsafe { cmd.pre_exec(seccomp_lock) }; let status = cmd - .arg(&path) .spawn() .map_err(|e| format!("Failed to launch editor: {e}"))? .wait_with_output()?; @@ -509,13 +169,13 @@ where debug!("Current file position: {seek_pos}"); file.rewind()?; debug!("Rewound temporary file for reading"); - match serde_json::from_reader::<_, Versioning>(&mut file) { + match serde_json::from_reader::<_, T>(&mut file) { Ok(new_config) => { - warn_anomalies(&new_config, &mut warn_handler); + new_config.warn_anomalies(&mut warn_handler); debug!("config: {new_config:#?}"); let after = serde_json::to_string_pretty(&new_config)?; writeln!(output, "Resulting confguration: {after}")?; - let after = serde_json::from_str::>(&after)?; + let after = serde_json::from_str::(&after)?; debug!("re-serialised: {after:#?}"); // Yes == save, No and edit again == continue loop, abort == return false writeln!( @@ -539,7 +199,7 @@ where return Ok(false); } // else save and exit - *config.as_ref().borrow_mut() = new_config.data; + *config = new_config; return Ok(true); } Err(e) => { @@ -565,13 +225,20 @@ where #[cfg(test)] mod tests { - use rar_common::database::structs::{SCommand, SConfig, SetBehavior}; - use rar_common::{RemoteStorageSettings, SettingsContent, StorageMethod}; + use rar_common::database::actor::SActor; + use rar_common::database::structs::{ + SCommand, SCommands, SCredentials, SPolicy, SRole, STask, SetBehavior, + }; + use rar_common::file::RootSettings; + use rar_common::util::StorageMethod; + use rar_common::{RemoteStorageSettings, SettingsContent}; use super::*; + use std::cell::RefCell; use std::fs; use std::io::Cursor; use std::os::unix::fs::PermissionsExt; + use std::rc::Rc; use std::time::{SystemTime, UNIX_EPOCH}; #[test] @@ -589,7 +256,7 @@ mod tests { let _ = fs::remove_dir_all(&temp_dir_path_clone); }); - let config = Rc::new(RefCell::new(FullSettings::default())); + let mut config = RootSettings::default(); // Create a mock editor script let mock_editor_path = temp_dir_path.join("mock_editor.sh"); @@ -601,8 +268,8 @@ for last; do true; done file="$last" echo '{}' > "$file" "#, - serde_json::to_string_pretty(&Versioning::new(Rc::new(RefCell::new( - FullSettings::builder() + serde_json::to_string_pretty(&Rc::new(RefCell::new( + RootSettings::builder() .storage( SettingsContent::builder() .method(StorageMethod::JSON) @@ -615,7 +282,7 @@ echo '{}' > "$file" .build(), ) .config( - SConfig::builder() + SPolicy::builder() .role( SRole::builder("test_role") .actor(SActor::user(0).build()) @@ -638,7 +305,7 @@ echo '{}' > "$file" .build(), ) .build(), - )))) + ))) .unwrap() ); fs::write(&mock_editor_path, script).unwrap(); @@ -650,9 +317,9 @@ echo '{}' > "$file" let mut output = Vec::new(); let mut warnings = Vec::new(); - let result = edit_config_internal( + let result = edit_internal( &temp_dir_path, - &config, + &mut config, mock_editor_path.to_str().unwrap(), &mut input, &mut output, @@ -671,7 +338,10 @@ echo '{}' > "$file" ); assert!(output_str.contains("Is this configuration valid?")); - assert!(warnings.is_empty(), "Expected no warnings"); + assert!( + warnings.is_empty(), + "Expected no warnings, but got: {warnings:?}" + ); } #[test] @@ -689,7 +359,7 @@ echo '{}' > "$file" let _ = fs::remove_dir_all(&temp_dir_path_clone); }); - let config = Rc::new(RefCell::new(FullSettings::default())); + let mut config = RootSettings::default(); let mock_editor_path = temp_dir_path.join("mock_editor.sh"); let script = r#"#!/bin/sh @@ -704,9 +374,9 @@ echo '{ "version": "1.0.0", "storage": { "method": "json" }, "unknown_config_fie let mut input = Cursor::new(input_data); let mut output = Vec::new(); - let result = edit_config_internal( + let result = edit_internal( &temp_dir_path, - &config, + &mut config, mock_editor_path.to_str().unwrap(), &mut input, &mut output, @@ -732,7 +402,7 @@ echo '{ "version": "1.0.0", "storage": { "method": "json" }, "unknown_config_fie let _ = fs::remove_dir_all(&temp_dir_path_clone); }); - let config = Rc::new(RefCell::new(FullSettings::default())); + let mut config = RootSettings::default(); let mock_editor_path = temp_dir_path.join("mock_editor.sh"); let script = r#"#!/bin/sh @@ -747,9 +417,9 @@ echo '{ "version": "1.0.0", "storage": { "method": "json" }, mistake }' > "$fil let mut input = Cursor::new(input_data); let mut output = Vec::new(); - let result = edit_config_internal( + let result = edit_internal( &temp_dir_path, - &config, + &mut config, mock_editor_path.to_str().unwrap(), &mut input, &mut output, @@ -775,13 +445,13 @@ echo '{ "version": "1.0.0", "storage": { "method": "json" }, mistake }' > "$fil let _ = fs::remove_dir_all(&temp_dir_path_clone); }); - let config = Rc::new(RefCell::new(FullSettings::default())); + let mut config = RootSettings::default(); let mock_editor_path = temp_dir_path.join("mock_editor.sh"); let script = r#"#!/bin/sh for last; do true; done file="$last" -echo '{ "version": "1.0.0", "storage": { "method": "json" } }' > "$file" +echo '{ "storage": { "method": "json" } }' > "$file" "#; fs::write(&mock_editor_path, script).unwrap(); fs::set_permissions(&mock_editor_path, fs::Permissions::from_mode(0o755)).unwrap(); @@ -791,9 +461,9 @@ echo '{ "version": "1.0.0", "storage": { "method": "json" } }' > "$file" let mut output = Vec::new(); let mut warnings = Vec::new(); - let result = edit_config_internal( + let result = edit_internal( &temp_dir_path, - &config, + &mut config, mock_editor_path.to_str().unwrap(), &mut input, &mut output, @@ -824,7 +494,7 @@ echo '{ "version": "1.0.0", "storage": { "method": "json" } }' > "$file" let _ = fs::remove_dir_all(&temp_dir_path_clone); }); - let config = Rc::new(RefCell::new(FullSettings::default())); + let mut config = RootSettings::default(); let mock_editor_path = temp_dir_path.join("mock_editor.sh"); // We construct a JSON with many unknown fields to trigger warnings @@ -927,9 +597,9 @@ EOF let mut output = Vec::new(); let mut warnings = Vec::new(); - let result = edit_config_internal( + let result = edit_internal( &temp_dir_path, - &config, + &mut config, mock_editor_path.to_str().unwrap(), &mut input, &mut output, diff --git a/src/chsr/cli/mod.rs b/src/chsr/cli/mod.rs index 30354751..8163f6fa 100644 --- a/src/chsr/cli/mod.rs +++ b/src/chsr/cli/mod.rs @@ -8,7 +8,7 @@ pub mod process; #[cfg(not(tarpaulin_include))] pub mod usage; -use std::{cell::RefCell, error::Error, path::Path, rc::Rc}; +use std::{error::Error, path::PathBuf}; use bon::builder; use data::{Cli, Inputs, Rule}; @@ -18,17 +18,17 @@ use log::debug; use pair::recurse_pair; use pest::Parser; use process::process_input; -use rar_common::FullSettings; +use rar_common::file::FileSettings; use usage::print_usage; -use crate::{cli::editor::edit_config, util::escape_parser_string_vec}; +use crate::{cli::editor::start_editing, util::escape_parser_string_vec}; #[builder] pub fn main( - #[builder(start_fn)] storage: &Rc>, + #[builder(start_fn)] storage: &mut FileSettings, #[builder(start_fn)] args: I, #[builder(default = RulesetStatus::NotEnforced)] ruleset: RulesetStatus, - folder: Option<&Path>, + folder: Option<&PathBuf>, ) -> Result> where I: IntoIterator, @@ -51,31 +51,39 @@ where if ruleset == RulesetStatus::NotEnforced { return Err("Editor mode requires landlock to be enforced.".into()); } - return edit_config(folder.expect("implementation error"), storage); + if let Some(path) = inputs.editor_path { + if let Some(storage) = storage.get(&path) { + return start_editing(&path, &mut *storage.as_ref().borrow_mut()); + } + let file = FileSettings::read_policy(&path, inputs.editor_type.unwrap_or_default())?; + return start_editing(&path, &mut *file.data.data.as_ref().borrow_mut()); + } + return start_editing( + folder.expect("implementation error"), + &mut *storage.get_root_mut(), + ); } process_input(storage, inputs) } #[cfg(test)] mod tests { - use std::{env::current_dir, fs, io::Write}; + use std::{cell::RefCell, fs, rc::Rc}; use indexmap::IndexSet; use rar_common::{ - FullSettings, RemoteStorageSettings, SettingsContent, StorageMethod, + RemoteStorageSettings, SettingsContent, database::{ - actor::SActor, - actor::SGroups, + actor::{SActor, SGroups}, options::*, structs::{SCredentials, *}, - versionning::Versioning, }, - read_full_settings, - util::remove_with_privileges, + file::RootSettings, + util::{StorageMethod, remove_with_privileges}, }; use serde_json::{Map, Value}; - use crate::ROOTASROLE; + use crate::util::RAR_CFG_PATH; use super::*; use capctl::Cap; @@ -105,7 +113,7 @@ mod tests { // Test helper functions struct TestContext { - settings: Rc>, + settings: FileSettings, role_index: usize, task_index: usize, } @@ -113,8 +121,9 @@ mod tests { impl TestContext { fn new(name: &str) -> (Self, Defer) { let defer = setup(name); - let path = format!("{ROOTASROLE}.{name}"); - let settings = read_full_settings(&path).expect("Failed to get settings"); + let path = format!("{RAR_CFG_PATH}.{name}"); + let settings = FileSettings::write_all(path.clone(), path, StorageMethod::JSON) + .expect("should work"); ( Self { settings, @@ -125,18 +134,18 @@ mod tests { ) } - fn run_command(&self, command: &str) -> Result> { - main(&self.settings, command.split(' ')) + fn run_command(&mut self, command: &str) -> Result> { + main(&mut self.settings, command.split(' ')) .call() .inspect_err(|e| error!("{e}")) .inspect(|e| debug!("{e}")) } - fn assert_command_success(&self, command: &str) { + fn assert_command_success(&mut self, command: &str) { assert!(self.run_command(command).expect("Command should not fail")); } - fn assert_command_no_change(&self, command: &str) { + fn assert_command_no_change(&mut self, command: &str) { assert!(!self.run_command(command).expect("Command should not fail")); } @@ -153,7 +162,7 @@ mod tests { role_ref.options.as_ref().unwrap().clone() } Level::Global => { - let settings_ref = self.settings.as_ref().borrow(); + let settings_ref = self.settings.get_root(); let config_ref = settings_ref.config.as_ref().unwrap().as_ref().borrow(); config_ref.options.as_ref().unwrap().clone() } @@ -162,7 +171,7 @@ mod tests { } fn get_role(&self, role_index: usize) -> Rc> { - let settings_ref = self.settings.as_ref().borrow(); + let settings_ref = self.settings.get_root(); let config_ref = settings_ref.config.as_ref().unwrap().as_ref().borrow(); config_ref[role_index].clone() } @@ -254,14 +263,14 @@ mod tests { }); } - fn run_command_vec(&self, args: Vec<&str>) -> Result> { - main(&self.settings, args) + fn run_command_vec(&mut self, args: Vec<&str>) -> Result> { + main(&mut self.settings, args) .call() .inspect_err(|e| error!("{e}")) .inspect(|e| debug!("{e}")) } - fn assert_command_vec_success(&self, args: Vec<&str>) { + fn assert_command_vec_success(&mut self, args: Vec<&str>) { assert!(self.run_command_vec(args).expect("Command should not fail")); } @@ -422,6 +431,107 @@ mod tests { }); } + fn with_workdir_set(&self, f: F) -> R + where + F: FnOnce(&SWorkdirSet) -> R, + { + let settings_ref = self.opt(Level::Task); + let task_ref = settings_ref.as_ref().borrow(); + let workdir = task_ref.workdir.as_ref().expect("workdir expected"); + match workdir { + SWorkdirEither::Struct(workdir_set) => f(workdir_set), + SWorkdirEither::Path(_) => panic!("workdir set expected"), + } + } + + fn assert_workdir_is_path(&self, expected: &str) { + let settings_ref = self.opt(Level::Task); + let task_ref = settings_ref.as_ref().borrow(); + let workdir = task_ref.workdir.as_ref().expect("workdir expected"); + match workdir { + SWorkdirEither::Path(path) => assert_eq!(path, expected), + SWorkdirEither::Struct(workdir_set) => { + assert_eq!(workdir_set.fallback.as_deref(), Some(expected)); + } + } + } + + fn assert_workdir_default_behavior(&self, expected: WorkdirBehavior) { + self.with_workdir_set(|workdir_set| { + assert_eq!(workdir_set.default_behavior, expected); + }); + } + + fn assert_workdir_whitelist_contains(&self, path: &str) { + self.with_workdir_set(|workdir_set| { + let default = IndexSet::new(); + assert!( + workdir_set + .add + .as_ref() + .unwrap_or(&default) + .contains(&path.to_string()) + ); + }); + } + + fn assert_workdir_whitelist_not_contains(&self, path: &str) { + self.with_workdir_set(|workdir_set| { + let default = IndexSet::new(); + assert!( + !workdir_set + .add + .as_ref() + .unwrap_or(&default) + .contains(&path.to_string()) + ); + }); + } + + fn assert_workdir_blacklist_contains(&self, path: &str) { + self.with_workdir_set(|workdir_set| { + let default = IndexSet::new(); + assert!( + workdir_set + .sub + .as_ref() + .unwrap_or(&default) + .contains(&path.to_string()) + ); + }); + } + + fn assert_workdir_blacklist_not_contains(&self, path: &str) { + self.with_workdir_set(|workdir_set| { + let default = IndexSet::new(); + assert!( + !workdir_set + .sub + .as_ref() + .unwrap_or(&default) + .contains(&path.to_string()) + ); + }); + } + + fn assert_workdir_whitelist_is_empty(&self) { + self.assert_workdir_whitelist_len(0); + } + + fn assert_workdir_whitelist_len(&self, expected: usize) { + self.with_workdir_set(|workdir_set| { + let default = IndexSet::new(); + assert_eq!(workdir_set.add.as_ref().unwrap_or(&default).len(), expected); + }); + } + + fn assert_workdir_blacklist_len(&self, expected: usize) { + self.with_workdir_set(|workdir_set| { + let default = IndexSet::new(); + assert_eq!(workdir_set.sub.as_ref().unwrap_or(&default).len(), expected); + }); + } + fn with_env_options(&self, f: F) -> R where F: FnOnce(&SEnvOptions) -> R, @@ -634,174 +744,174 @@ mod tests { #[allow(clippy::too_many_lines)] fn setup(name: &str) -> Defer { - let file_path = format!("{ROOTASROLE}.{name}"); - let versionned = Versioning::new( - FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .settings( - RemoteStorageSettings::builder() - .path(file_path.clone()) - .not_immutable() - .build(), - ) - .build(), - ) - .config( - SConfig::builder() - .options(|opt| { - opt.timeout( - STimeout::builder() - .type_field(TimestampType::PPID) - .duration( - TimeDelta::hours(15) - .checked_add(&TimeDelta::minutes(30)) - .unwrap() - .checked_add(&TimeDelta::seconds(30)) - .unwrap(), - ) - .max_usage(1) - .build(), - ) - .path( - SPathOptions::builder(PathBehavior::Delete) - .add(["path1", "path2"]) - .sub(["path3", "path4"]) - .build(), - ) - .env( - SEnvOptions::builder(EnvBehavior::Delete) - .keep(["env1", "env2"]) - .unwrap() - .check(["env3", "env4"]) - .unwrap() - .delete(["env5", "env6"]) - .unwrap() - .set([("env7", "val7"), ("env8", "val8")]) + let file_path = format!("{RAR_CFG_PATH}.{name}"); + println!("Setting up test with file path: {file_path}"); + let mut versionned = FileSettings::builder() + .root( + file_path.clone().into(), + RootSettings::builder() + .storage( + SettingsContent::builder() + .method(StorageMethod::JSON) + .settings( + RemoteStorageSettings::builder() + .path(file_path.clone()) + .not_immutable() .build(), ) - .root(SPrivileged::Privileged) - .bounding(SBounding::Ignore) - .build() - }) - .role( - SRole::builder("complete") - .options(|opt| { - opt.timeout( - STimeout::builder() - .type_field(TimestampType::PPID) - .duration( - TimeDelta::hours(15) - .checked_add(&TimeDelta::minutes(30)) - .unwrap() - .checked_add(&TimeDelta::seconds(30)) - .unwrap(), - ) - .max_usage(1) - .build(), - ) - .path( - SPathOptions::builder(PathBehavior::Delete) - .add(["path1", "path2"]) - .sub(["path3", "path4"]) - .build(), - ) - .env( - SEnvOptions::builder(EnvBehavior::Delete) - .keep(["env1", "env2"]) - .unwrap() - .check(["env3", "env4"]) - .unwrap() - .delete(["env5", "env6"]) - .unwrap() - .set([("env7", "val7"), ("env8", "val8")]) - .build(), - ) - .root(SPrivileged::Privileged) - .bounding(SBounding::Ignore) - .build() - }) - .actor(SActor::user(0).build()) - .actor(SActor::group(0).build()) - .actor(SActor::group(["groupA", "groupB"]).build()) - .task( - STask::builder("t_complete") - .options(|opt| { - opt.timeout( - STimeout::builder() - .type_field(TimestampType::PPID) - .duration( - TimeDelta::hours(15) - .checked_add(&TimeDelta::minutes(30)) - .unwrap() - .checked_add(&TimeDelta::seconds(30)) - .unwrap(), - ) - .max_usage(1) - .build(), - ) - .path( - SPathOptions::builder(PathBehavior::Delete) - .add(["path1", "path2"]) - .sub(["path3", "path4"]) - .build(), - ) - .env( - SEnvOptions::builder(EnvBehavior::Delete) - .keep(["env1", "env2"]) - .unwrap() - .check(["env3", "env4"]) - .unwrap() - .delete(["env5", "env6"]) - .unwrap() - .set([("env7", "val7"), ("env8", "val8")]) - .build(), - ) - .root(SPrivileged::Privileged) - .bounding(SBounding::Ignore) - .build() - }) - .commands( - SCommands::builder(SetBehavior::All) - .add(["ls".into(), "echo".into()]) - .sub(["cat".into(), "grep".into()]) - .build(), + .build(), + ) + .config( + SPolicy::builder() + .options(|opt| { + opt.timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration( + TimeDelta::hours(15) + .checked_add(&TimeDelta::minutes(30)) + .unwrap() + .checked_add(&TimeDelta::seconds(30)) + .unwrap(), ) - .cred( - SCredentials::builder() - .setuid("user1") - .setgid(SGroupsEither::MandatoryGroups( - SGroups::from(["setgid1", "setgid2"]), - )) - .capabilities( - SCapabilities::builder(SetBehavior::All) - .add_cap(Cap::LINUX_IMMUTABLE) - .add_cap(Cap::NET_BIND_SERVICE) - .sub_cap(Cap::SYS_ADMIN) - .sub_cap(Cap::SYS_BOOT) - .build(), + .max_usage(1) + .build(), + ) + .path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1", "path2"]) + .sub(["path3", "path4"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env1", "env2"]) + .unwrap() + .check(["env3", "env4"]) + .unwrap() + .delete(["env5", "env6"]) + .unwrap() + .set([("env7", "val7"), ("env8", "val8")]) + .build(), + ) + .root(SPrivileged::Privileged) + .bounding(SBounding::Ignore) + .build() + }) + .role( + SRole::builder("complete") + .options(|opt| { + opt.timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration( + TimeDelta::hours(15) + .checked_add(&TimeDelta::minutes(30)) + .unwrap() + .checked_add(&TimeDelta::seconds(30)) + .unwrap(), ) + .max_usage(1) .build(), ) - .build(), - ) - .build(), - ) - .build(), - ) - .build(), - ); - let mut file = std::fs::File::create(file_path.clone()).unwrap_or_else(|_| { - panic!( - "Failed to create {:?}/{:?} file at", - current_dir().unwrap(), - file_path + .path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1", "path2"]) + .sub(["path3", "path4"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env1", "env2"]) + .unwrap() + .check(["env3", "env4"]) + .unwrap() + .delete(["env5", "env6"]) + .unwrap() + .set([("env7", "val7"), ("env8", "val8")]) + .build(), + ) + .root(SPrivileged::Privileged) + .bounding(SBounding::Ignore) + .build() + }) + .actor(SActor::user(0).build()) + .actor(SActor::group(0).build()) + .actor(SActor::group(["groupA", "groupB"]).build()) + .task( + STask::builder("t_complete") + .options(|opt| { + opt.timeout( + STimeout::builder() + .type_field(TimestampType::PPID) + .duration( + TimeDelta::hours(15) + .checked_add(&TimeDelta::minutes( + 30, + )) + .unwrap() + .checked_add(&TimeDelta::seconds( + 30, + )) + .unwrap(), + ) + .max_usage(1) + .build(), + ) + .path( + SPathOptions::builder(PathBehavior::Delete) + .add(["path1", "path2"]) + .sub(["path3", "path4"]) + .build(), + ) + .env( + SEnvOptions::builder(EnvBehavior::Delete) + .keep(["env1", "env2"]) + .unwrap() + .check(["env3", "env4"]) + .unwrap() + .delete(["env5", "env6"]) + .unwrap() + .set([("env7", "val7"), ("env8", "val8")]) + .build(), + ) + .root(SPrivileged::Privileged) + .bounding(SBounding::Ignore) + .build() + }) + .commands( + SCommands::builder(SetBehavior::All) + .add(["ls".into(), "echo".into()]) + .sub(["cat".into(), "grep".into()]) + .build(), + ) + .cred( + SCredentials::builder() + .setuid("user1") + .setgid(SGroupsEither::MandatoryGroups( + SGroups::from(["setgid1", "setgid2"]), + )) + .capabilities( + SCapabilities::builder(SetBehavior::All) + .add_cap(Cap::LINUX_IMMUTABLE) + .add_cap(Cap::NET_BIND_SERVICE) + .sub_cap(Cap::SYS_ADMIN) + .sub_cap(Cap::SYS_BOOT) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build(), ) - }); - let jsonstr = serde_json::to_string_pretty(&versionned).unwrap(); - file.write_all(jsonstr.as_bytes()).unwrap(); - file.flush().unwrap(); + .unwrap() + .build(); + versionned.save_all().unwrap(); defer(move || { remove_with_privileges(file_path).unwrap(); }) @@ -840,9 +950,13 @@ mod tests { // chsr o timeout set --type tty --duration 5:00 --max_usage 1 // chsr o t unset --type --duration --max_usage + // chsr o workdir set /home/user + // chsr o workdir setpolicy (all|none|inherit) + // chsr o workdir (whitelist|blacklist) (add|del|set|purge) /home/user/** + #[test] fn test_all_main() { - let (ctx, _defer) = TestContext::new("all_main"); + let (mut ctx, _defer) = TestContext::new("all_main"); // Test --help command (should not change anything) ctx.assert_command_no_change("--help"); @@ -855,7 +969,7 @@ mod tests { } #[test] fn test_r_complete_show_actors() { - let (ctx, _defer) = TestContext::new("r_complete_show_actors"); + let (mut ctx, _defer) = TestContext::new("r_complete_show_actors"); // Test show commands (should not change anything) ctx.assert_command_no_change("r complete show actors"); @@ -867,21 +981,22 @@ mod tests { } #[test] fn test_purge_tasks() { - let (ctx, _defer) = TestContext::new("purge_tasks"); + let (mut ctx, _defer) = TestContext::new("purge_tasks"); // Test purge tasks command (should make changes) ctx.assert_command_success("r complete purge tasks"); } #[test] fn test_r_complete_purge_all() { - let (ctx, _defer) = TestContext::new("r_complete_purge_all"); + let (mut ctx, _defer) = TestContext::new("r_complete_purge_all"); // Test purge all command (should make changes) ctx.assert_command_success("r complete purge all"); } #[test] fn test_r_complete_grant_u_user1_g_group1_g_group2_group3() { - let (ctx, _defer) = TestContext::new("r_complete_grant_u_user1_g_group1_g_group2_group3"); + let (mut ctx, _defer) = + TestContext::new("r_complete_grant_u_user1_g_group1_g_group2_group3"); // Test grant command (should make changes) ctx.assert_command_success("r complete grant -u user1 -g group1 -g group2&group3"); @@ -901,7 +1016,7 @@ mod tests { } #[test] fn test_r_complete_task_t_complete_show_all() { - let (ctx, _defer) = TestContext::new("r_complete_task_t_complete_show_all"); + let (mut ctx, _defer) = TestContext::new("r_complete_task_t_complete_show_all"); // Test show commands (should not change anything) ctx.assert_command_no_change("r complete task t_complete show all"); @@ -913,14 +1028,14 @@ mod tests { } #[test] fn test_r_complete_task_t_complete_purge_cmd() { - let (ctx, _defer) = TestContext::new("r_complete_task_t_complete_purge_cmd"); + let (mut ctx, _defer) = TestContext::new("r_complete_task_t_complete_purge_cmd"); // Test purge cmd command (should make changes) ctx.assert_command_success("r complete task t_complete purge cmd"); } #[test] fn test_r_complete_task_t_complete_purge_cred() { - let (ctx, _defer) = TestContext::new("r_complete_task_t_complete_purge_cred"); + let (mut ctx, _defer) = TestContext::new("r_complete_task_t_complete_purge_cred"); // Test purge cred command (should make changes) ctx.assert_command_success("r complete task t_complete purge cred"); @@ -935,21 +1050,21 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_cmd_setpolicy_deny_all() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_cmd_setpolicy_deny_all"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_cmd_setpolicy_deny_all"); ctx.assert_command_success("r complete t t_complete cmd setpolicy deny-all"); ctx.assert_command_default_behavior(Some(SetBehavior::None)); } #[test] fn test_r_complete_t_t_complete_cmd_setpolicy_allow_all() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_cmd_setpolicy_allow_all"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_cmd_setpolicy_allow_all"); ctx.assert_command_success("r complete t t_complete cmd setpolicy allow-all"); ctx.assert_command_default_behavior(Some(SetBehavior::All)); } #[test] fn test_r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces() { - let (ctx, _defer) = + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_cmd_whitelist_add_super_command_with_spaces"); let command = SCommand::Simple("super command with spaces".to_string()); @@ -974,7 +1089,7 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces() { - let (ctx, _defer) = + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_cmd_blacklist_del_super_command_with_spaces"); let command = SCommand::Simple("super command with spaces".to_string()); @@ -998,7 +1113,7 @@ mod tests { #[test] fn test_r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2() { - let (ctx, _defer) = TestContext::new( + let (mut ctx, _defer) = TestContext::new( "r_complete_t_t_complete_cred_set_caps_cap_dac_override_cap_sys_admin_cap_sys_boot_setuid_user1_setgid_group1_group2", ); @@ -1023,7 +1138,7 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_cred_caps_setpolicy_deny_all() { - let (ctx, _defer) = + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_cred_caps_setpolicy_deny_all"); ctx.assert_command_success("r complete t t_complete cred caps setpolicy deny-all"); @@ -1031,7 +1146,7 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_cred_caps_setpolicy_allow_all() { - let (ctx, _defer) = + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_cred_caps_setpolicy_allow_all"); ctx.assert_command_success("r complete t t_complete cred caps setpolicy allow-all"); @@ -1040,7 +1155,7 @@ mod tests { #[test] fn test_r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot() { - let (ctx, _defer) = TestContext::new( + let (mut ctx, _defer) = TestContext::new( "r_complete_t_t_complete_cred_caps_whitelist_add_cap_dac_override_cap_sys_admin_cap_sys_boot", ); @@ -1052,7 +1167,7 @@ mod tests { #[test] fn test_r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot() { - let (ctx, _defer) = TestContext::new( + let (mut ctx, _defer) = TestContext::new( "r_complete_t_t_complete_cred_caps_blacklist_add_cap_dac_override_cap_sys_admin_cap_sys_boot", ); @@ -1078,7 +1193,7 @@ mod tests { } #[test] fn test_options_show_all() { - let (ctx, _defer) = TestContext::new("options_show_all"); + let (mut ctx, _defer) = TestContext::new("options_show_all"); // Test show commands (should not change anything) ctx.assert_command_no_change("options show all"); @@ -1087,7 +1202,7 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_options_show_env() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_options_show_env"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_options_show_env"); // Test show commands (should not change anything) ctx.assert_command_no_change("r complete t t_complete options show env"); @@ -1099,14 +1214,15 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_path_setpolicy_delete_all() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_path_setpolicy_delete_all"); + let (mut ctx, _defer) = + TestContext::new("r_complete_t_t_complete_o_path_setpolicy_delete_all"); ctx.assert_command_success("r complete t t_complete o path setpolicy delete-all"); ctx.assert_path_default_behavior(PathBehavior::Delete); } #[test] fn test_r_complete_t_t_complete_o_path_setpolicy_keep_unsafe() { - let (ctx, _defer) = + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_path_setpolicy_keep_unsafe"); ctx.assert_command_success("r complete t t_complete o path setpolicy keep-unsafe"); @@ -1121,7 +1237,7 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_path_whitelist_add() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_path_whitelist_add"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_path_whitelist_add"); // Test whitelist add ctx.assert_command_success("r complete t t_complete o path whitelist add /usr/bin:/bin"); @@ -1163,13 +1279,78 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_path_blacklist_purge() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_path_blacklist_purge"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_path_blacklist_purge"); ctx.assert_command_success("r complete t t_complete o path blacklist purge"); } + + #[test] + fn test_r_complete_t_t_complete_o_workdir_set() { + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_workdir_set"); + + ctx.assert_command_success("r complete t t_complete o workdir set /home/user"); + ctx.assert_workdir_is_path("/home/user"); + } + + #[test] + fn test_r_complete_t_t_complete_o_workdir_setpolicy() { + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_workdir_setpolicy"); + + ctx.assert_command_success("r complete t t_complete o workdir setpolicy all"); + ctx.assert_workdir_default_behavior(WorkdirBehavior::Blacklist); + + ctx.assert_command_success("r complete t t_complete o workdir setpolicy none"); + ctx.assert_workdir_default_behavior(WorkdirBehavior::Allowlist); + + ctx.assert_command_success("r complete t t_complete o workdir setpolicy inherit"); + ctx.assert_workdir_default_behavior(WorkdirBehavior::Inherit); + } + + #[test] + fn test_r_complete_t_t_complete_o_workdir_whitelist_add() { + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_workdir_whitelist_add"); + + ctx.assert_command_success("r complete t t_complete o workdir whitelist add /home/user"); + ctx.assert_workdir_whitelist_contains("/home/user"); + + ctx.assert_command_success("r complete t t_complete o workdir whitelist del /home/user"); + ctx.assert_workdir_whitelist_not_contains("/home/user"); + + ctx.assert_command_success( + "r complete t t_complete o workdir whitelist set /home/user:/tmp", + ); + ctx.assert_workdir_whitelist_contains("/home/user"); + ctx.assert_workdir_whitelist_contains("/tmp"); + ctx.assert_workdir_whitelist_len(2); + + ctx.assert_command_success("r complete t t_complete o workdir whitelist purge"); + ctx.assert_workdir_whitelist_is_empty(); + } + + #[test] + fn test_r_complete_t_t_complete_o_workdir_blacklist_add() { + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_workdir_blacklist_add"); + + ctx.assert_command_success("r complete t t_complete o workdir blacklist set /home/user"); + ctx.assert_workdir_blacklist_contains("/home/user"); + ctx.assert_workdir_blacklist_len(1); + + ctx.assert_command_success("r complete t t_complete o workdir blacklist add /tmp"); + ctx.assert_workdir_blacklist_contains("/tmp"); + ctx.assert_workdir_blacklist_len(2); + + ctx.assert_command_success("r complete t t_complete o workdir blacklist del /home/user"); + ctx.assert_workdir_blacklist_not_contains("/home/user"); + ctx.assert_workdir_blacklist_contains("/tmp"); + ctx.assert_workdir_blacklist_len(1); + + ctx.assert_command_success("r complete t t_complete o workdir blacklist purge"); + ctx.assert_workdir_blacklist_len(0); + } #[test] fn test_r_complete_t_t_complete_o_env_keep_only_myvar_var2() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2"); + let (mut ctx, _defer) = + TestContext::new("r_complete_t_t_complete_o_env_keep_only_MYVAR_VAR2"); ctx.assert_command_success("r complete t t_complete o env keep-only MYVAR,VAR2"); ctx.assert_env_default_behavior_is_delete(); @@ -1179,7 +1360,7 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_env_delete_only_myvar_var2() { - let (ctx, _defer) = + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_delete_only_MYVAR_VAR2"); ctx.assert_command_success("r complete t t_complete o env delete-only MYVAR,VAR2"); @@ -1190,7 +1371,7 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_env_set_myvar_value_var2_value2() { - let (ctx, _defer) = + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_set_MYVAR_value_VAR2_value2"); ctx.assert_command_success( @@ -1202,7 +1383,7 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_env_add_myvar_value_var2_value2() { - let (ctx, _defer) = + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_add_MYVAR_value_VAR2_value2"); // Test setlist set @@ -1229,28 +1410,31 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_env_setpolicy_delete_all() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_setpolicy_delete_all"); + let (mut ctx, _defer) = + TestContext::new("r_complete_t_t_complete_o_env_setpolicy_delete_all"); ctx.assert_command_success("r complete t t_complete o env setpolicy delete-all"); ctx.assert_env_default_behavior(EnvBehavior::Delete); } #[test] fn test_r_complete_t_t_complete_o_env_setpolicy_keep_all() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_setpolicy_keep_all"); + let (mut ctx, _defer) = + TestContext::new("r_complete_t_t_complete_o_env_setpolicy_keep_all"); ctx.assert_command_success("r complete t t_complete o env setpolicy keep-all"); ctx.assert_env_default_behavior(EnvBehavior::Keep); } #[test] fn test_r_complete_t_t_complete_o_env_setpolicy_inherit() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_setpolicy_inherit"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_setpolicy_inherit"); ctx.assert_command_success("r complete t t_complete o env setpolicy inherit"); ctx.assert_env_default_behavior(EnvBehavior::Inherit); } #[test] fn test_r_complete_t_t_complete_o_env_whitelist_add_myvar() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_whitelist_add_MYVAR"); + let (mut ctx, _defer) = + TestContext::new("r_complete_t_t_complete_o_env_whitelist_add_MYVAR"); // Test whitelist add ctx.assert_command_success("r complete t t_complete o env whitelist add MYVAR"); @@ -1269,14 +1453,15 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_env_whitelist_purge() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_whitelist_purge"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_whitelist_purge"); ctx.assert_command_success("r complete t t_complete o env whitelist purge"); ctx.assert_env_keep_is_none(); } #[test] fn test_r_complete_t_t_complete_o_env_blacklist_add_myvar() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_blacklist_add_MYVAR"); + let (mut ctx, _defer) = + TestContext::new("r_complete_t_t_complete_o_env_blacklist_add_MYVAR"); // Test blacklist add ctx.assert_command_success("r complete t t_complete o env blacklist add MYVAR"); @@ -1288,7 +1473,8 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_env_blacklist_set_myvar() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_blacklist_set_MYVAR"); + let (mut ctx, _defer) = + TestContext::new("r_complete_t_t_complete_o_env_blacklist_set_MYVAR"); ctx.assert_command_success("r complete t t_complete o env blacklist set MYVAR"); ctx.assert_env_delete_contains("MYVAR"); @@ -1296,14 +1482,15 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_env_blacklist_purge() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_blacklist_purge"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_blacklist_purge"); ctx.assert_command_success("r complete t t_complete o env blacklist purge"); ctx.assert_env_delete_is_none(); } #[test] fn test_r_complete_t_t_complete_o_env_checklist_add_myvar() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_env_checklist_add_MYVAR"); + let (mut ctx, _defer) = + TestContext::new("r_complete_t_t_complete_o_env_checklist_add_MYVAR"); // Test checklist add ctx.assert_command_success("r complete t t_complete o env checklist add MYVAR"); @@ -1327,7 +1514,7 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_root_privileged() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_root_privileged"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_root_privileged"); // Test root privileged ctx.assert_command_success("r complete t t_complete o root privileged"); @@ -1345,28 +1532,28 @@ mod tests { } #[test] fn test_r_complete_t_t_complete_o_bounding_strict() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_bounding_strict"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_bounding_strict"); ctx.assert_command_success("r complete t t_complete o bounding strict"); ctx.assert_bounding_option(Some(&SBounding::Strict)); } #[test] fn test_r_complete_t_t_complete_o_bounding_ignore() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_bounding_ignore"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_bounding_ignore"); ctx.assert_command_success("r complete t t_complete o bounding ignore"); ctx.assert_bounding_option(Some(&SBounding::Ignore)); } #[test] fn test_r_complete_t_t_complete_o_bounding_inherit() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_bounding_inherit"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_bounding_inherit"); ctx.assert_command_success("r complete t t_complete o bounding unset"); ctx.assert_bounding_option(None); } #[test] fn test_r_complete_t_t_complete_o_auth_skip() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_auth_skip"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_auth_skip"); // Test auth skip ctx.assert_command_success("r complete t t_complete o auth skip"); @@ -1385,7 +1572,7 @@ mod tests { #[test] fn test_r_complete_t_t_complete_o_execinfo() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_execinfo"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_execinfo"); // Test execinfo set ctx.assert_command_success("r complete t t_complete o execinfo show"); @@ -1402,7 +1589,7 @@ mod tests { #[test] fn test_r_complete_t_t_complete_o_umask() { - let (ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_umask"); + let (mut ctx, _defer) = TestContext::new("r_complete_t_t_complete_o_umask"); // Test umask set ctx.assert_command_success("r complete t t_complete o umask 027"); @@ -1437,40 +1624,39 @@ mod tests { .filter_level(log::LevelFilter::Debug) .is_test(true) .try_init(); - let (ctx, _defer) = TestContext::new("convert"); + let (mut ctx, _defer) = TestContext::new("convert"); - ctx.assert_command_success(&format!("convert cbor {ROOTASROLE}.convert.bin")); - ctx.assert_command_success(&format!("convert json {ROOTASROLE}.convert.json.1")); + ctx.assert_command_success(&format!("convert cbor {RAR_CFG_PATH}.convert.bin")); + ctx.assert_command_success(&format!("convert json {RAR_CFG_PATH}.convert.json.1")); - assert!(fs::metadata(format!("{ROOTASROLE}.convert.bin")).is_ok()); + assert!(fs::metadata(format!("{RAR_CFG_PATH}.convert.bin")).is_ok()); ctx.assert_command_success(&format!( - "convert --from cbor {ROOTASROLE}.convert.bin json {ROOTASROLE}.convert.json" + "convert --from cbor {RAR_CFG_PATH}.convert.bin json {RAR_CFG_PATH}.convert.json" )); - assert!(fs::metadata(format!("{ROOTASROLE}.convert.json")).is_ok()); + assert!(fs::metadata(format!("{RAR_CFG_PATH}.convert.json")).is_ok()); assert_eq!( normalize_json_object( serde_json::from_str::( - &fs::read_to_string(format!("{ROOTASROLE}.convert.json")).unwrap() + &fs::read_to_string(format!("{RAR_CFG_PATH}.convert.json")).unwrap() ) .unwrap() ), normalize_json_object( serde_json::from_str::( - &fs::read_to_string(format!("{ROOTASROLE}.convert.json.1")).unwrap() + &fs::read_to_string(format!("{RAR_CFG_PATH}.convert.json.1")).unwrap() ) .unwrap() ) ); ctx.assert_command_success(&format!( - "convert --reconfigure cbor {ROOTASROLE}.reconfigure.convert.bin" + "convert --reconfigure cbor {RAR_CFG_PATH}.reconfigure.convert.bin" )); assert_eq!( ctx.settings - .as_ref() - .borrow() + .get_root() .storage .settings .as_ref() @@ -1480,10 +1666,10 @@ mod tests { .unwrap() .to_str() .unwrap(), - format!("{ROOTASROLE}.reconfigure.convert.bin") + format!("{RAR_CFG_PATH}.reconfigure.convert.bin") ); - fs::remove_file(format!("{ROOTASROLE}.convert.bin")).unwrap(); - fs::remove_file(format!("{ROOTASROLE}.convert.json")).unwrap(); + fs::remove_file(format!("{RAR_CFG_PATH}.convert.bin")).unwrap(); + fs::remove_file(format!("{RAR_CFG_PATH}.convert.json")).unwrap(); } } diff --git a/src/chsr/cli/pair.rs b/src/chsr/cli/pair.rs index dc13e071..40bcf073 100644 --- a/src/chsr/cli/pair.rs +++ b/src/chsr/cli/pair.rs @@ -9,12 +9,12 @@ use strum::VariantNames; use crate::cli::data::{Convertion, RoleType, TaskType}; use rar_common::{ - StorageMethod, database::{ actor::{SActor, SGroupType}, - options::{EnvBehavior, OptType, PathBehavior, TimestampType}, + options::{EnvBehavior, OptType, PathBehavior, TimestampType, WorkdirBehavior}, structs::{IdTask, SetBehavior}, }, + util::StorageMethod, }; use super::data::{InputAction, Inputs, Rule, SetListType, TimeoutOpt}; @@ -163,6 +163,10 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box { + inputs.action = InputAction::Set; + inputs.options_workdir_policy = Some(WorkdirBehavior::const_parse(pair.as_str())); + } // === timeout === Rule::time => { let mut reversed = pair.as_str().split(':').rev(); @@ -221,6 +225,13 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box { inputs.timeout_max_usage = Some(pair.as_str().parse::()?); } + // === file === + Rule::file => { + inputs.policy = true; + } + Rule::policy_path => { + inputs.policy_path = Some(pair.as_str().to_string()); + } // === roles === Rule::role_id => { inputs.role_id = Some(pair.as_str().to_string()); @@ -362,6 +373,9 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box { inputs.options_type = Some(OptType::Path); } + Rule::opt_workdir => { + inputs.options_type = Some(OptType::Workdir); + } Rule::opt_show_arg => { if pair.as_str() == "all" { inputs.options_type = None; @@ -444,6 +458,27 @@ fn match_pair(pair: &Pair, inputs: &mut Inputs) -> Result<(), Box { + let mut inner = pair.clone().into_inner(); + inputs.editor_type = Some( + inner + .next() + .expect("from_type not found") + .as_str() + .to_lowercase() + .parse() + .inspect_err(|&e| { + warn!( + "Unknown type {}, types available : {}", + e, + StorageMethod::VARIANTS.join(", ") + ); + })?, + ); + } + Rule::editor_path => { + inputs.editor_path = Some(pair.as_str().into()); + } _ => { debug!("Unmatched rule: {:?}", pair.as_rule()); } diff --git a/src/chsr/cli/process.rs b/src/chsr/cli/process.rs index 7fdc6adf..beb7be66 100644 --- a/src/chsr/cli/process.rs +++ b/src/chsr/cli/process.rs @@ -14,11 +14,15 @@ use json::{ use log::debug; use rar_common::{ - FullSettings, database::{ options::{Opt, OptType}, structs::{IdTask, RoleGetter}, }, + file::FileSettings, +}; + +use crate::cli::process::json::{ + workdir_purge, workdir_set_path, workdir_setlist, workdir_setpolicy, }; use super::{ @@ -27,10 +31,7 @@ use super::{ }; #[allow(clippy::too_many_lines)] -pub fn process_input( - storage: &Rc>, - inputs: Inputs, -) -> Result> { +pub fn process_input(storage: &mut FileSettings, inputs: Inputs) -> Result> { if inputs.action == InputAction::Convert { debug!("chsr convert"); return convert::convert( @@ -41,8 +42,11 @@ pub fn process_input( inputs.convert_reconfigure, ); } - let binding = storage.as_ref().borrow(); - let rconfig = binding.config.as_ref().ok_or("No configuration loaded")?; + let rconfig = storage + .get_root() + .config + .as_ref() + .ok_or("No configuration loaded")?; match inputs { Inputs { action: InputAction::Help, @@ -329,6 +333,17 @@ pub fn process_input( .. } => path_purge(rconfig, role_id.as_ref(), task_id, setlist_type), + Inputs { + // chsr o path whitelist set a:b:c + action: InputAction::Purge, + role_id, + task_id, + options_path: None, + options_type: Some(OptType::Workdir), + setlist_type, + .. + } => workdir_purge(rconfig, role_id.as_ref(), task_id, setlist_type), + Inputs { // chsr o env whitelist set A,B,C action: InputAction::Set, @@ -388,7 +403,17 @@ pub fn process_input( .. } => path_setpolicy(rconfig, role_id.as_ref(), task_id, options_path_policy), Inputs { - // chsr o path whitelist add path1:path2:path3 + // chsr o workdir setpolicy none + action: InputAction::Set, + role_id, + task_id, + options_type: Some(OptType::Workdir), + options_workdir_policy: Some(options_workdir_policy), + .. + } => workdir_setpolicy(rconfig, role_id.as_ref(), task_id, options_workdir_policy), + + Inputs { + // chsr o env whitelist add A action, role_id, task_id, @@ -426,6 +451,35 @@ pub fn process_input( &options_path, ), + Inputs { + // chsr o workdir set /home/user + action: InputAction::Set, + role_id, + task_id, + options_path: Some(options_path), + options_type: Some(OptType::Workdir), + setlist_type: None, + .. + } => workdir_set_path(rconfig, role_id.as_ref(), task_id, options_path.as_str()), + + Inputs { + // chsr o workdir whitelist add /home/user + action, + role_id, + task_id, + options_path: Some(options_path), + options_type: Some(OptType::Workdir), + setlist_type, + .. + } => workdir_setlist( + rconfig, + role_id.as_ref(), + task_id, + setlist_type, + action, + &options_path, + ), + Inputs { // chsr o env setpolicy delete-all role_id, @@ -439,7 +493,7 @@ pub fn process_input( } } pub fn perform_on_target_opt( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, exec_on_opt: impl Fn(Rc>) -> Result<(), Box>, diff --git a/src/chsr/cli/process/convert.rs b/src/chsr/cli/process/convert.rs index eba63e56..8c4dc0dc 100644 --- a/src/chsr/cli/process/convert.rs +++ b/src/chsr/cli/process/convert.rs @@ -1,37 +1,38 @@ use std::{ - cell::RefCell, error::Error, - fs::File, - io::{BufWriter, Write}, path::{Path, PathBuf}, - rc::Rc, }; use log::{debug, error}; use rar_common::{ - FullSettings, RemoteStorageSettings, StorageMethod, database::versionning::Versioning, - retrieve_sconfig, + RemoteStorageSettings, + database::versionning::Versioning, + file::{FileSettings, LockedSettingsFile}, + util::RAR_CFG_TYPE, }; -use crate::{ROOTASROLE, cli::data::Convertion}; +use crate::{ + cli::data::Convertion, + util::{RAR_CFG_IMMUTABLE, RAR_CFG_PATH}, +}; pub fn convert( - settings: &Rc>, + settings: &mut FileSettings, convertion: Convertion, convert_reconfigure: bool, ) -> Result> { debug!("chsr convert"); - let mut settings = settings.borrow_mut(); let default = RemoteStorageSettings::default(); - let default_path = PathBuf::default(); - let path = settings + let binding = PathBuf::from(RAR_CFG_PATH); + let rar_data_path = settings + .get_root() .storage .settings .as_ref() .unwrap_or(&default) .path .as_ref() - .unwrap_or(&default_path); + .unwrap_or(&binding); let config = match convertion.from { Some(ref from) => { debug!("Convert from: {}", from.display()); @@ -40,38 +41,55 @@ pub fn convert( error!("The source and destination paths are the same"); return Ok(false); } - if from == path { + if from == rar_data_path { settings + .get_root() .config .as_ref() .expect("A configuration should be loaded") .clone() } else { - retrieve_sconfig(&from_type, from)? + FileSettings::read_policy(from, from_type)?.data.data } } None => settings + .get_root() .config - .clone() - .expect("A configuration should be loaded"), + .as_ref() + .expect("A configuration should be loaded") + .clone(), }; println!( "Config : {}", serde_json::to_string_pretty(&Versioning::new(config.clone()))? ); - if !convert_reconfigure && convertion.to != *path { - write_config_file(&convertion, config) + if !convert_reconfigure && convertion.to != *rar_data_path { + debug!( + "Writing configuration to new file : {}", + convertion.to.display() + ); + let mut c = LockedSettingsFile::open_write(convertion.to, |_, _| { + Ok(Versioning::new(config.clone())) + })?; + c.save(convertion.to_type, RAR_CFG_IMMUTABLE)?; + + Ok(true) } else if convert_reconfigure { - if convertion.to_type != StorageMethod::JSON && convertion.to == Path::new(ROOTASROLE) { + if convertion.to_type != RAR_CFG_TYPE && convertion.to == Path::new(RAR_CFG_PATH) { error!( - "The general settings file cannot be converted to another format than JSON\nThis file is used to determine the policy location and format. Please specify another path." + "The general settings file cannot be converted to another format than {RAR_CFG_TYPE}\nThis file is used to determine the policy location and format. Please specify another path.", ); return Ok(false); } - settings.storage.method = convertion.to_type; - let mut remote = settings.storage.settings.clone().unwrap_or_default(); - remote.path = Some(convertion.to); - settings.storage.settings = Some(remote); + debug!("Overwriting current configuration file"); + settings.get_root_mut().storage.method = convertion.to_type; + settings + .get_root_mut() + .storage + .settings + .get_or_insert_default() + .path + .replace(convertion.to); Ok(true) } else { error!( @@ -80,23 +98,3 @@ pub fn convert( Ok(false) } } - -fn write_config_file( - convertion: &Convertion, - config: Rc>, -) -> Result> { - match convertion.to_type { - StorageMethod::JSON => { - let json = serde_json::to_string_pretty(&Versioning::new(config))?; - let file = File::create(&convertion.to)?; - let mut writer = BufWriter::new(file); - writer.write_all(json.as_bytes())?; - } - StorageMethod::CBOR => { - let file = File::create(&convertion.to)?; - let writer = BufWriter::new(file); - cbor4ii::serde::to_writer(writer, &Versioning::new(config))?; - } - } - Ok(true) -} diff --git a/src/chsr/cli/process/json.rs b/src/chsr/cli/process/json.rs index fdc8d7bb..28114a0c 100644 --- a/src/chsr/cli/process/json.rs +++ b/src/chsr/cli/process/json.rs @@ -8,7 +8,7 @@ use crate::cli::data::{InputAction, RoleType, SetListType, TaskType, TimeoutOpt} use rar_common::database::{ options::{ EnvBehavior, EnvKey, Opt, OptStack, OptType, PathBehavior, SEnvOptions, SPathOptions, - STimeout, SUMask, + STimeout, SUMask, SWorkdirEither, SWorkdirSet, WorkdirBehavior, }, structs::{ IdTask, RoleGetter, SCapabilities, SCommand, SGroupsEither, SRole, STask, SUserEither, @@ -18,7 +18,7 @@ use rar_common::database::{ use super::perform_on_target_opt; pub fn list_json( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, options: bool, @@ -57,7 +57,9 @@ fn list_task( if let Some(task) = role.as_ref().borrow().task(&task_id) { if options { debug!("task {task:?}"); - let rcopt = OptStack::from_task(&task.clone()).to_opt(); + let rcopt = OptStack::from_task(&task.clone()) + .to_opt() + .map_err(std::io::Error::other)?; let opt = rcopt.as_ref().borrow(); if let Some(opttype) = options_type { match opttype { @@ -85,6 +87,9 @@ fn list_task( OptType::UMask => { println!("{}", serde_json::to_string_pretty(&opt.umask)?); } + OptType::Workdir => { + println!("{}", serde_json::to_string_pretty(&opt.workdir)?); + } } } else { println!("{}", serde_json::to_string_pretty(&rcopt)?); @@ -96,10 +101,10 @@ fn list_task( return Err("Task not found".into()); } } else if options { - println!( - "{}", - serde_json::to_string_pretty(&OptStack::from_role(&role.clone()).to_opt())? - ); + let rcopt = OptStack::from_role(&role.clone()) + .to_opt() + .map_err(std::io::Error::other)?; + println!("{}", serde_json::to_string_pretty(&rcopt)?); } else { print_role(role, role_type.unwrap_or(RoleType::All))?; } @@ -155,7 +160,7 @@ fn print_role( } pub fn role_add_del( - rconfig: &Rc>, + rconfig: &Rc>, action: InputAction, role_id: String, role_type: Option, @@ -209,7 +214,7 @@ pub fn role_add_del( } pub fn task_add_del( - rconfig: &Rc>, + rconfig: &Rc>, role_id: &str, action: InputAction, task_id: IdTask, @@ -281,7 +286,7 @@ pub fn task_add_del( } pub fn grant_revoke( - rconfig: &Rc>, + rconfig: &Rc>, role_id: &str, action: InputAction, mut actors: Vec, @@ -319,7 +324,7 @@ pub fn grant_revoke( } pub fn cred_set( - rconfig: &Rc>, + rconfig: &Rc>, role_id: &str, task_id: IdTask, cred_caps: Option, @@ -346,7 +351,7 @@ pub fn cred_set( } pub fn cred_unset( - rconfig: &Rc>, + rconfig: &Rc>, role_id: &str, task_id: IdTask, cred_caps: Option, @@ -378,7 +383,7 @@ pub fn cred_unset( } pub fn cred_caps( - rconfig: &Rc>, + rconfig: &Rc>, role_id: &str, task_id: IdTask, setlist_type: SetListType, @@ -448,7 +453,7 @@ pub fn cred_caps( } pub fn cred_setpolicy( - rconfig: &Rc>, + rconfig: &Rc>, role_id: &str, task_id: IdTask, cred_policy: rar_common::database::structs::SetBehavior, @@ -472,7 +477,7 @@ pub fn cred_setpolicy( } pub fn cmd_whitelist_action( - rconfig: &Rc>, + rconfig: &Rc>, role_id: &str, task_id: IdTask, cmd_id: impl IntoIterator, @@ -530,7 +535,7 @@ pub fn cmd_whitelist_action( } pub fn cmd_setpolicy( - rconfig: &Rc>, + rconfig: &Rc>, role_id: &str, task_id: IdTask, cmd_policy: rar_common::database::structs::SetBehavior, @@ -546,7 +551,7 @@ pub fn cmd_setpolicy( } pub fn env_set_policylist( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, options_env: &IndexSet, @@ -574,12 +579,12 @@ pub fn env_set_policylist( } pub fn set_privileged( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, options_root: Option, ) -> Result> { - debug!("chsr o root set privileged"); + debug!("chsr o root set privileged {options_root:?}"); perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { opt.as_ref().borrow_mut().root = options_root; Ok(()) @@ -588,12 +593,12 @@ pub fn set_privileged( } pub fn set_bounding( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, options_bounding: Option, ) -> Result> { - debug!("chsr o bounding set"); + debug!("chsr o bounding set {options_bounding:?}"); perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { opt.as_ref().borrow_mut().bounding = options_bounding; Ok(()) @@ -602,12 +607,12 @@ pub fn set_bounding( } pub fn set_authentication( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, options_auth: Option, ) -> Result> { - debug!("chsr o auth set"); + debug!("chsr o auth set {options_auth:?}"); perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { opt.as_ref().borrow_mut().authentication = options_auth; Ok(()) @@ -616,12 +621,12 @@ pub fn set_authentication( } pub fn set_execinfo( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, options_execinfo: Option, ) -> Result> { - debug!("chsr o execinfo set"); + debug!("chsr o execinfo set {options_execinfo:?}"); perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { opt.as_ref().borrow_mut().execinfo = options_execinfo; Ok(()) @@ -630,12 +635,12 @@ pub fn set_execinfo( } pub fn set_umask( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, options_umask: Option, ) -> Result> { - debug!("chsr o umask set"); + debug!("chsr o umask set {options_umask:?}"); perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { opt.as_ref().borrow_mut().umask = options_umask; Ok(()) @@ -644,7 +649,7 @@ pub fn set_umask( } pub fn path_set( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, setlist_type: Option, @@ -688,7 +693,7 @@ pub fn path_set( } pub fn path_purge( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, setlist_type: Option, @@ -715,8 +720,45 @@ pub fn path_purge( Ok(true) } +pub fn workdir_purge( + rconfig: &Rc>, + role_id: Option<&String>, + task_id: Option, + setlist_type: Option, +) -> Result> { + debug!("chsr o workdir purge"); + perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { + let mut binding = opt.as_ref().borrow_mut(); + let workdir_current = binding.workdir.take(); + let mut workdir_set = match workdir_current { + Some(SWorkdirEither::Struct(workdir_set)) => workdir_set, + Some(SWorkdirEither::Path(path)) => SWorkdirSet { + fallback: Some(path), + ..Default::default() + }, + None => SWorkdirSet::default(), + }; + match setlist_type { + Some(SetListType::White) => { + if let Some(add) = &mut workdir_set.add { + add.clear(); + } + } + Some(SetListType::Black) => { + if let Some(sub) = &mut workdir_set.sub { + sub.clear(); + } + } + _ => unreachable!("Unknown setlist type"), + } + binding.workdir = Some(SWorkdirEither::Struct(workdir_set)); + Ok(()) + })?; + Ok(true) +} + pub fn env_whitelist_set( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, setlist_type: Option<&SetListType>, @@ -748,7 +790,7 @@ pub fn env_whitelist_set( } pub fn unset_timeout( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, timeout_arg: [bool; 3], @@ -777,7 +819,7 @@ pub fn unset_timeout( } pub fn set_timeout( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, timeout_type: Option, @@ -803,7 +845,7 @@ pub fn set_timeout( } pub fn path_setlist2( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, setlist_type: Option, @@ -889,8 +931,117 @@ pub fn path_setlist2( Ok(true) } +pub fn workdir_set_path( + rconfig: &Rc>, + role_id: Option<&String>, + task_id: Option, + options_path: &str, +) -> Result> { + debug!("chsr o workdir set path {options_path}"); + perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { + opt.as_ref().borrow_mut().workdir = Some(SWorkdirEither::Path(options_path.to_string())); + Ok(()) + })?; + Ok(true) +} + +pub fn workdir_setlist( + rconfig: &Rc>, + role_id: Option<&String>, + task_id: Option, + setlist_type: Option, + action: InputAction, + options_path: &str, +) -> Result> { + debug!("chsr o w set whitelist|blacklist add|del|set path1:path2:path3"); + perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { + let mut binding = opt.as_ref().borrow_mut(); + let workdir_current = binding.workdir.take(); + let mut workdir_set = match workdir_current { + Some(SWorkdirEither::Struct(workdir_set)) => workdir_set, + Some(SWorkdirEither::Path(path)) => SWorkdirSet { + fallback: Some(path), + ..Default::default() + }, + None => SWorkdirSet::default(), + }; + match setlist_type { + Some(SetListType::White) => match action { + InputAction::Add => { + workdir_set.add.get_or_insert_with(IndexSet::new).extend( + options_path + .split(':') + .map(std::string::ToString::to_string), + ); + } + InputAction::Del => { + debug!("workdir.add del {:?}", workdir_set.add); + let hashset = options_path + .split(':') + .map(std::string::ToString::to_string) + .collect::>(); + if let Some(paths) = &mut workdir_set.add { + *paths = paths + .difference(&hashset) + .cloned() + .collect::>(); + } else { + warn!("No path to remove from del list"); + } + } + InputAction::Set => { + workdir_set.add = Some( + options_path + .split(':') + .map(std::string::ToString::to_string) + .collect(), + ); + } + _ => unreachable!("Unknown action {:?}", action), + }, + Some(SetListType::Black) => match action { + InputAction::Add => { + workdir_set.sub.get_or_insert_with(IndexSet::new).extend( + options_path + .split(':') + .map(std::string::ToString::to_string), + ); + } + InputAction::Del => { + debug!("workdir.del del {:?}", workdir_set.sub); + let hashset = options_path + .split(':') + .map(std::string::ToString::to_string) + .collect::>(); + if let Some(paths) = &mut workdir_set.sub { + *paths = paths + .difference(&hashset) + .cloned() + .collect::>(); + } else { + warn!("No path to remove from del list"); + } + } + InputAction::Set => { + workdir_set.sub = Some( + options_path + .split(':') + .map(std::string::ToString::to_string) + .collect(), + ); + } + _ => unreachable!("Unknown action {:?}", action), + }, + _ => unreachable!("Unknown setlist type"), + } + binding.workdir = Some(SWorkdirEither::Struct(workdir_set)); + Ok(()) + })?; + Ok(true) +} + pub fn path_setpolicy( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, options_path_policy: PathBehavior, @@ -910,9 +1061,45 @@ pub fn path_setpolicy( .map(|()| true) } +pub fn workdir_setpolicy( + rconfig: &Rc>, + role_id: Option<&String>, + task_id: Option, + options_workdir_policy: WorkdirBehavior, +) -> Result> { + debug!("chsr o path setpolicy delete-all"); + perform_on_target_opt(rconfig, role_id, task_id, |opt: Rc>| { + let workdir = opt.as_ref().borrow_mut().workdir.as_mut().map_or_else( + || { + SWorkdirEither::Struct(SWorkdirSet { + default_behavior: options_workdir_policy, + fallback: None, + add: None, + sub: None, + }) + }, + |workdir| match workdir { + SWorkdirEither::Path(p) => SWorkdirEither::Struct(SWorkdirSet { + default_behavior: options_workdir_policy, + fallback: Some(p.clone()), + add: None, + sub: None, + }), + SWorkdirEither::Struct(sworkdir_set) => { + sworkdir_set.default_behavior = options_workdir_policy; + SWorkdirEither::Struct(sworkdir_set.clone()) + } + }, + ); + opt.as_ref().borrow_mut().workdir = Some(workdir); + Ok(()) + }) + .map(|()| true) +} + #[allow(clippy::too_many_lines)] pub fn env_setlist_add( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, setlist_type: Option, @@ -1039,16 +1226,15 @@ pub fn env_setlist_add( } pub fn env_setpolicy( - rconfig: &Rc>, + rconfig: &Rc>, role_id: Option<&String>, task_id: Option, options_env_policy: EnvBehavior, ) -> Result> { - debug!("chsr o env setpolicy delete-all"); + debug!("chsr o env setpolicy delete-all {options_env_policy:?}"); 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); + let env = binding.env.get_or_insert_default(); env.default_behavior = options_env_policy; Ok(()) })?; diff --git a/src/chsr/cli/usage.rs b/src/chsr/cli/usage.rs index befcb68b..f940fa1e 100644 --- a/src/chsr/cli/usage.rs +++ b/src/chsr/cli/usage.rs @@ -1,156 +1,632 @@ -use std::error::Error; +use std::{error::Error, fmt::Write}; -use const_format::formatcp; use log::debug; use super::data::Rule; use crate::util::underline; use rar_common::util::{BOLD, RED, RST, UNDERLINE}; -const LONG_ABOUT: &str = "Role Manager is a tool to configure RBAC for RootAsRole. -A role is a set of tasks that can be executed by a user or a group of users. -These tasks are multiple commands associated with their granted permissions (credentials). -Like Sudo, you could manipulate environment variables, PATH, and other options. -More than Sudo, you can manage the capabilities and remove privileges from the root user."; - -const RAR_USAGE_GENERAL: &str = formatcp!("{UNDERLINE}{BOLD}Usage:{RST} {BOLD}chsr{RST} [command] [options] - -{UNDERLINE}{BOLD}Commands:{RST} - {BOLD}-h, --help{RST} Show help for commands and options. - {BOLD}list, show, l{RST} List available items; use with specific commands for detailed views. - {BOLD}role, r{RST} Manage roles and related operations. -",UNDERLINE=UNDERLINE, BOLD=BOLD, RST=RST); - -const RAR_USAGE_ROLE: &str = formatcp!("{UNDERLINE}{BOLD}Role Operations:{RST} -chsr role [role_name] [operation] [options] - {BOLD}add, create{RST} Add a new role. - {BOLD}del, delete, unset, d, rm{RST} Delete a specified role. - {BOLD}show, list, l{RST} Show details of a specified role (actors, tasks, all). - {BOLD}purge{RST} Remove all items from a role (actors, tasks, all). - - {BOLD}grant{RST} Grant permissions to a user or group. - {BOLD}revoke{RST} Revoke permissions from a user or group. - {BOLD}-u, --user{RST} [user_name] Specify a user for grant or revoke operations. - {BOLD}-g, --group{RST} [group_names] Specify one or more groups combinaison for grant or revoke operations. -",UNDERLINE=UNDERLINE, BOLD=BOLD, RST=RST); - -const RAR_USAGE_TASK: &str = formatcp!("{UNDERLINE}{BOLD}Task Operations:{RST} -chsr role [role_name] task [task_name] [operation] - {BOLD}show, list, l{RST} Show task details (all, cmd, cred). - {BOLD}purge{RST} Purge configurations or credentials of a task (all, cmd, cred). - {BOLD}add, create{RST} Add a new task. - {BOLD}del, delete, unset, d, rm{RST} Remove a task. -",UNDERLINE=UNDERLINE, BOLD=BOLD, RST=RST); - -const RAR_USAGE_CMD: &str = formatcp!( - "{UNDERLINE}{BOLD}Command Operations:{RST} -chsr role [role_name] task [task_name] command [cmd] - {BOLD}show{RST} Show commands. - {BOLD}setpolicy{RST} [policy] Set policy for commands (allow-all, deny-all). - {BOLD}whitelist, wl{RST} [listing] Manage the whitelist for commands. - {BOLD}blacklist, bl{RST} [listing] Manage the blacklist for commands. -", - UNDERLINE = UNDERLINE, - BOLD = BOLD, - RST = RST -); - -const RAR_USAGE_CRED: &str = formatcp!( - "{UNDERLINE}{BOLD}Credentials Operations:{RST} -chsr role [role_name] task [task_name] credentials [operation] - {BOLD}show{RST} Show credentials. - {BOLD}set, unset{RST} Set or unset credentials details. - {BOLD}caps{RST} Manage capabilities for credentials. -", - UNDERLINE = UNDERLINE, - BOLD = BOLD, - RST = RST -); - -const RAR_USAGE_CRED_CAPS: &str = formatcp!( - "{UNDERLINE}{BOLD}Capabilities Operations:{RST} -chsr role [role_name] task [task_name] credentials caps [operation] - {BOLD}setpolicy{RST} [policy] Set policy for capabilities (allow-all, deny-all). - {BOLD}whitelist, wl{RST} [listing] Manage whitelist for credentials. - {BOLD}blacklist, bl{RST} [listing] Manage blacklist for credentials. -", - UNDERLINE = UNDERLINE, - BOLD = BOLD, - RST = RST -); - -const RAR_USAGE_OPTIONS_GENERAL :&str = formatcp!("{UNDERLINE}{BOLD}Options:{RST} -chsr options [option] [operation] -chsr role [role_name] options [option] [operation] -chsr role [role_name] task [task_name] options [option] [operation] - {BOLD}path{RST} Manage path settings (set, whitelist, blacklist). - {BOLD}env{RST} Manage environment variable settings (set, whitelist, blacklist, checklist). - {BOLD}root{RST} [policy] Defines when the root user (uid == 0) gets his privileges by default. (unset, privileged, user, inherit) - {BOLD}bounding{RST} [policy] Defines when dropped capabilities are permanently removed in the instantiated process. (unset, strict, ignore, inherit) - {BOLD}timeout{RST} Manage timeout settings (set, unset). - {BOLD}authentication{RST} [policy] Defines if user needs to authenticate (unset, skip, perform, inherit). - {BOLD}execinfo{RST} [policy] Defines if user can see execution settings (unset, display, hide, inherit). - {BOLD}umask, mask{RST} [del|umask] Defines the umask for the executed command (unset or 022). -",UNDERLINE=UNDERLINE, BOLD=BOLD, RST=RST); - -const RAR_USAGE_OPTIONS_PATH :&str = formatcp!("{UNDERLINE}{BOLD}Path options:{RST} -chsr options path [operation] - {BOLD}setpolicy{RST} [policy] Specify the policy for path settings (delete-all, keep-safe, keep-unsafe, inherit). - {BOLD}set{RST} [path] Set the policy as delete-all and the path to enforce. - {BOLD}whitelist, wl{RST} [listing] Manage the whitelist for path settings. - {BOLD}blacklist, bl{RST} [listing] Manage the blacklist for path settings. -",UNDERLINE=UNDERLINE, BOLD=BOLD, RST=RST); - -const RAR_USAGE_OPTIONS_ENV :&str = formatcp!("{UNDERLINE}{BOLD}Environment options:{RST} -chsr options env [operation] - {BOLD}setpolicy{RST} [policy] Specify the policy for environment settings (delete-all, keep-all, inherit). - {BOLD}set{RST} [key=value,...] Set variables to enforce. - {BOLD}keep-only{RST} [key,...] Set the policy as delete-all and the key map to keep. - {BOLD}delete-only{RST} [key,...] Set the policy as keep-all and the key map to delete. - {BOLD}whitelist, wl{RST} [listing] Manage the whitelist for environment settings. - {BOLD}blacklist, bl{RST} [listing] Manage the blacklist for environment settings. - {BOLD}checklist, cl{RST} [listing] Manage the checklist for environment settings. (Removed if contains unsafe chars) - {BOLD}setlist, sl{RST} [listing] Manage the setlist for environment settings. (define environment variables) -",UNDERLINE=UNDERLINE, BOLD=BOLD, RST=RST); - -const RAR_USAGE_OPTIONS_TIMEOUT: &str = formatcp!( - "{UNDERLINE}{BOLD}Timeout options:{RST} -chsr options timeout [operation] - {BOLD}set, unset{RST} Set or unset timeout settings. - {BOLD}--type{RST} [tty, ppid, uid] Specify the type of timeout. - {BOLD}--duration{RST} [HH:MM:SS] Specify the duration of the timeout. - {BOLD}--max-usage{RST} [number] Specify the maximum usage of the timeout.", - UNDERLINE = UNDERLINE, - BOLD = BOLD, - RST = RST -); - -const RAR_USAGE_LISTING: &str = formatcp!( - "{UNDERLINE}{BOLD}Listing:{RST} - add [items,...] Add items to the list. - del [items,...] Remove items from the list. - set [items,...] Set items in the list. - purge Remove all items from the list.", - UNDERLINE = UNDERLINE, - BOLD = BOLD, - RST = RST -); - -const RAR_CONVERT: &str = formatcp!( - "{UNDERLINE}{BOLD}Convert policy format :{RST} -chsr convert (-r) (--from [from_type] [from_file]) [to_type] [to_file] -Supported types: json, cbor - {BOLD}-r, --reconfigure{RST} Reconfigure /etc/security/rootasrole.json file to specify the new location. -{BOLD}Warning{RST}: the new location should be under a protected directory. - {BOLD}--from{RST} [from_type] [from_file] Specify the type and file to convert from.", - UNDERLINE = UNDERLINE, - BOLD = BOLD, - RST = RST); +const LONG_ABOUT: &str = " +chsr allows you to manage RootAsRole policies through a command-line interface. +The main idea in this CLI is to provide individual commands for each operation +and use a consistent syntax for options management across different levels +(global, role, task). +"; + +#[derive(Debug, Clone, Copy)] +struct UsageItem { + name: &'static str, + description: &'static str, + indent: usize, +} + +#[derive(Debug, Clone, Copy)] +struct UsageSection { + title: &'static str, + synopsis: &'static [&'static str], + items: &'static [UsageItem], + examples: &'static [&'static str], +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum UsageSectionId { + Usage, + Commands, + Role, + Task, + Command, + Credentials, + CredentialsCaps, + OptionsGeneral, + OptionsPath, + OptionsWorkdir, + OptionsEnv, + OptionsTimeout, + OptionsAuth, + OptionsExecinfo, + OptionsUmask, + Listing, + Convert, +} + +const USAGE_GENERAL: UsageSection = UsageSection { + title: "Usage", + synopsis: &["chsr [command] [options]"], + items: &[], + examples: &[], +}; + +const USAGE_COMMANDS: UsageSection = UsageSection { + title: "Commands", + synopsis: &[], + items: &[ + UsageItem { + name: "-h, --help", + description: "Show help for commands and options.", + indent: 1, + }, + UsageItem { + name: "list, show, l", + description: "List available items; use with specific commands for detailed views.", + indent: 1, + }, + UsageItem { + name: "role, r", + description: "Manage roles and related operations.", + indent: 1, + }, + ], + examples: &["chsr --help", "chsr list"], +}; + +const USAGE_ROLE: UsageSection = UsageSection { + title: "Role Operations", + synopsis: &["chsr role [role_name] [operation] [options]"], + items: &[ + UsageItem { + name: "add, create", + description: "Add a new role.", + indent: 1, + }, + UsageItem { + name: "del, delete, unset, d, rm", + description: "Delete a specified role.", + indent: 1, + }, + UsageItem { + name: "show, list, l", + description: "Show details of a specified role (actors, tasks, all).", + indent: 1, + }, + UsageItem { + name: "purge", + description: "Remove all items from a role (actors, tasks, all).", + indent: 1, + }, + UsageItem { + name: "grant", + description: "Grant permissions to a user or group.", + indent: 1, + }, + UsageItem { + name: "revoke", + description: "Revoke permissions from a user or group.", + indent: 1, + }, + UsageItem { + name: "-u, --user [user_name]", + description: "Specify a user for grant or revoke operations.", + indent: 2, + }, + UsageItem { + name: "-g, --group [group_names]", + description: "Specify one or more group combinations for grant or revoke operations.", + indent: 2, + }, + ], + examples: &["chsr role admin add", "chsr role admin grant -u alice"], +}; + +const USAGE_TASK: UsageSection = UsageSection { + title: "Task Operations", + synopsis: &["chsr role [role_name] task [task_name] [operation]"], + items: &[ + UsageItem { + name: "show, list, l", + description: "Show task details (all, cmd, cred).", + indent: 1, + }, + UsageItem { + name: "purge", + description: "Purge configurations or credentials of a task (all, cmd, cred).", + indent: 1, + }, + UsageItem { + name: "add, create", + description: "Add a new task.", + indent: 1, + }, + UsageItem { + name: "del, delete, unset, d, rm", + description: "Remove a task.", + indent: 1, + }, + ], + examples: &[ + "chsr role admin task backup add", + "chsr role admin task backup show", + ], +}; + +const USAGE_COMMAND: UsageSection = UsageSection { + title: "Command Operations", + synopsis: &["chsr role [role_name] task [task_name] command [cmd]"], + items: &[ + UsageItem { + name: "show", + description: "Show commands.", + indent: 1, + }, + UsageItem { + name: "setpolicy [policy]", + description: "Set policy for commands (allow-all, deny-all).", + indent: 1, + }, + UsageItem { + name: "whitelist, wl [listing]", + description: "Manage the whitelist for commands.", + indent: 1, + }, + UsageItem { + name: "blacklist, bl [listing]", + description: "Manage the blacklist for commands.", + indent: 1, + }, + ], + examples: &["chsr role admin task backup command setpolicy allow-all"], +}; + +const USAGE_CREDENTIALS: UsageSection = UsageSection { + title: "Credentials Operations", + synopsis: &["chsr role [role_name] task [task_name] credentials [operation]"], + items: &[ + UsageItem { + name: "show", + description: "Show credentials.", + indent: 1, + }, + UsageItem { + name: "set, unset", + description: "Set or unset credentials details.", + indent: 1, + }, + UsageItem { + name: "caps", + description: "Manage capabilities for credentials.", + indent: 1, + }, + ], + examples: &["chsr role admin task backup credentials show"], +}; + +const USAGE_CREDENTIALS_CAPS: UsageSection = UsageSection { + title: "Capabilities Operations", + synopsis: &["chsr role [role_name] task [task_name] credentials caps [operation]"], + items: &[ + UsageItem { + name: "setpolicy [policy]", + description: "Set policy for capabilities (allow-all, deny-all).", + indent: 1, + }, + UsageItem { + name: "whitelist, wl [listing]", + description: "Manage whitelist for credentials.", + indent: 1, + }, + UsageItem { + name: "blacklist, bl [listing]", + description: "Manage blacklist for credentials.", + indent: 1, + }, + ], + examples: &["chsr role admin task backup credentials caps whitelist add cap_net_raw"], +}; + +const USAGE_OPTIONS_GENERAL: UsageSection = UsageSection { + title: "Options", + synopsis: &[ + "chsr options [option] [operation]", + "chsr role [role_name] options [option] [operation]", + "chsr role [role_name] task [task_name] options [option] [operation]", + ], + items: &[ + UsageItem { + name: "path", + description: "Manage path settings (set, whitelist, blacklist).", + indent: 1, + }, + UsageItem { + name: "workdir, w", + description: "Manage workdir settings (set, whitelist, blacklist).", + indent: 1, + }, + UsageItem { + name: "env", + description: "Manage environment variable settings (set, whitelist, blacklist, checklist).", + indent: 1, + }, + UsageItem { + name: "root [policy]", + description: "Defines when root gets privileges (unset, privileged, user, inherit).", + indent: 1, + }, + UsageItem { + name: "bounding [policy]", + description: "Defines how dropped capabilities are handled (unset, strict, ignore, inherit).", + indent: 1, + }, + UsageItem { + name: "timeout", + description: "Manage timeout settings (set, unset).", + indent: 1, + }, + UsageItem { + name: "authentication [policy]", + description: "Defines if user needs to authenticate (unset, skip, perform, inherit).", + indent: 1, + }, + UsageItem { + name: "execinfo [policy]", + description: "Defines if user can see execution settings (unset, display, hide, inherit).", + indent: 1, + }, + UsageItem { + name: "umask, mask [del|umask]", + description: "Defines the umask for executed command (unset or 022).", + indent: 1, + }, + ], + examples: &["chsr options path show", "chsr role admin options env show"], +}; + +const USAGE_OPTIONS_PATH: UsageSection = UsageSection { + title: "Path options", + synopsis: &["chsr options path [operation]"], + items: &[ + UsageItem { + name: "setpolicy [policy]", + description: "Specify the policy to use.", + indent: 1, + }, + UsageItem { + name: "set [path]", + description: "Enforce the specified path.", + indent: 1, + }, + UsageItem { + name: "whitelist, wl [listing]", + description: "Manage the whitelist settings.", + indent: 1, + }, + UsageItem { + name: "blacklist, bl [listing]", + description: "Manage the blacklist settings.", + indent: 1, + }, + ], + examples: &[ + "chsr options path setpolicy keep-safe", + "chsr options path whitelist add /usr/bin:/bin", + ], +}; + +const USAGE_OPTIONS_WORKDIR: UsageSection = UsageSection { + title: "Workdir options", + synopsis: &["chsr options workdir [operation]"], + items: &[ + UsageItem { + name: "setpolicy [policy]", + description: "Specify the policy for workdir settings (all, none, inherit).", + indent: 1, + }, + UsageItem { + name: "set [path]", + description: "Set the working directory path.", + indent: 1, + }, + UsageItem { + name: "whitelist, wl [listing]", + description: "Manage the whitelist for workdir settings.", + indent: 1, + }, + UsageItem { + name: "blacklist, bl [listing]", + description: "Manage the blacklist for workdir settings.", + indent: 1, + }, + ], + examples: &[ + "chsr options workdir setpolicy none", + "chsr options workdir set /home/user", + ], +}; + +const USAGE_OPTIONS_ENV: UsageSection = UsageSection { + title: "Environment options", + synopsis: &["chsr options env [operation]"], + items: &[ + UsageItem { + name: "setpolicy [policy]", + description: "Specify the policy for environment settings (delete-all, keep-all, inherit).", + indent: 1, + }, + UsageItem { + name: "set [key=value,...]", + description: "Set variables to enforce.", + indent: 1, + }, + UsageItem { + name: "keep-only [key,...]", + description: "Set policy as delete-all and key map to keep.", + indent: 1, + }, + UsageItem { + name: "delete-only [key,...]", + description: "Set policy as keep-all and key map to delete.", + indent: 1, + }, + UsageItem { + name: "whitelist, wl [listing]", + description: "Manage the whitelist for environment settings.", + indent: 1, + }, + UsageItem { + name: "blacklist, bl [listing]", + description: "Manage the blacklist for environment settings.", + indent: 1, + }, + UsageItem { + name: "checklist, cl [listing]", + description: "Manage checklist for environment settings (removed if unsafe).", + indent: 1, + }, + UsageItem { + name: "setlist, sl [listing]", + description: "Manage the setlist for environment settings.", + indent: 1, + }, + ], + examples: &[ + "chsr options env setpolicy keep-all", + "chsr options env keep-only PATH,HOME", + ], +}; + +const USAGE_OPTIONS_TIMEOUT: UsageSection = UsageSection { + title: "Timeout options", + synopsis: &["chsr options timeout [operation]"], + items: &[ + UsageItem { + name: "set, unset", + description: "Set or unset timeout settings.", + indent: 1, + }, + UsageItem { + name: "--type [tty, ppid, uid]", + description: "Specify the type of timeout.", + indent: 2, + }, + UsageItem { + name: "--duration [HH:MM:SS]", + description: "Specify the duration of the timeout.", + indent: 2, + }, + UsageItem { + name: "--max-usage [number]", + description: "Specify the maximum usage of the timeout.", + indent: 2, + }, + ], + examples: &["chsr options timeout set --type tty --duration 00:05:00 --max-usage 3"], +}; + +const USAGE_OPTIONS_AUTH: UsageSection = UsageSection { + title: "Authentication options", + synopsis: &["chsr options authentication [policy]"], + items: &[ + UsageItem { + name: "skip", + description: "Skip authentication.", + indent: 1, + }, + UsageItem { + name: "perform", + description: "Perform authentication.", + indent: 1, + }, + UsageItem { + name: "unset", + description: "Reset authentication behavior.", + indent: 1, + }, + ], + examples: &["chsr options authentication skip"], +}; + +const USAGE_OPTIONS_EXECINFO: UsageSection = UsageSection { + title: "Execution info options", + synopsis: &["chsr options execinfo [policy]"], + items: &[ + UsageItem { + name: "show", + description: "Display execution settings.", + indent: 1, + }, + UsageItem { + name: "hide", + description: "Hide execution settings.", + indent: 1, + }, + UsageItem { + name: "unset", + description: "Reset execution info behavior.", + indent: 1, + }, + ], + examples: &["chsr options execinfo hide"], +}; + +const USAGE_OPTIONS_UMASK: UsageSection = UsageSection { + title: "Umask options", + synopsis: &["chsr options umask [value]"], + items: &[ + UsageItem { + name: "del", + description: "Unset umask.", + indent: 1, + }, + UsageItem { + name: "0000-0777", + description: "Set umask value (octal).", + indent: 1, + }, + ], + examples: &["chsr options umask 022", "chsr options umask del"], +}; + +const USAGE_LISTING: UsageSection = UsageSection { + title: "Listing", + synopsis: &[], + items: &[ + UsageItem { + name: "add [items,...]", + description: "Add items to the list.", + indent: 1, + }, + UsageItem { + name: "del [items,...]", + description: "Remove items from the list.", + indent: 1, + }, + UsageItem { + name: "set [items,...]", + description: "Set items in the list.", + indent: 1, + }, + UsageItem { + name: "purge", + description: "Remove all items from the list.", + indent: 1, + }, + ], + examples: &["chsr options env whitelist add PATH,HOME"], +}; + +const USAGE_CONVERT: UsageSection = UsageSection { + title: "Convert policy format", + synopsis: &[ + "chsr convert (-r) (--from [from_type] [from_file]) [to_type] [to_file]", + "Supported types: json, cbor", + "Warning: the new location should be under a protected directory.", + ], + items: &[ + UsageItem { + name: "-r, --reconfigure", + description: "Reconfigure /etc/security/rootasrole.json to specify the new location.", + indent: 1, + }, + UsageItem { + name: "--from [from_type] [from_file]", + description: "Specify the type and file to convert from.", + indent: 1, + }, + ], + examples: &["chsr convert --from json /etc/security/rootasrole.json cbor /tmp/rar.cbor"], +}; + +const fn usage_section(id: UsageSectionId) -> &'static UsageSection { + match id { + UsageSectionId::Usage => &USAGE_GENERAL, + UsageSectionId::Commands => &USAGE_COMMANDS, + UsageSectionId::Role => &USAGE_ROLE, + UsageSectionId::Task => &USAGE_TASK, + UsageSectionId::Command => &USAGE_COMMAND, + UsageSectionId::Credentials => &USAGE_CREDENTIALS, + UsageSectionId::CredentialsCaps => &USAGE_CREDENTIALS_CAPS, + UsageSectionId::OptionsGeneral => &USAGE_OPTIONS_GENERAL, + UsageSectionId::OptionsPath => &USAGE_OPTIONS_PATH, + UsageSectionId::OptionsWorkdir => &USAGE_OPTIONS_WORKDIR, + UsageSectionId::OptionsEnv => &USAGE_OPTIONS_ENV, + UsageSectionId::OptionsTimeout => &USAGE_OPTIONS_TIMEOUT, + UsageSectionId::OptionsAuth => &USAGE_OPTIONS_AUTH, + UsageSectionId::OptionsExecinfo => &USAGE_OPTIONS_EXECINFO, + UsageSectionId::OptionsUmask => &USAGE_OPTIONS_UMASK, + UsageSectionId::Listing => &USAGE_LISTING, + UsageSectionId::Convert => &USAGE_CONVERT, + } +} + +fn render_section(section: &UsageSection) -> String { + let mut output = String::new(); + let _ = writeln!( + output, + "{UNDERLINE}{BOLD}{}:{RST}", + section.title, + UNDERLINE = UNDERLINE, + BOLD = BOLD, + RST = RST + ); + for line in section.synopsis { + output.push_str(line); + output.push('\n'); + } + let mut max_name_len = 0usize; + for item in section.items { + let len = item.name.chars().count(); + if len > max_name_len { + max_name_len = len; + } + } + for item in section.items { + let indent = " ".repeat(item.indent); + let padding = if max_name_len > item.name.chars().count() { + " ".repeat(max_name_len - item.name.chars().count()) + } else { + String::new() + }; + let _ = writeln!( + output, + "{}{}{}{}{} {}", + indent, BOLD, item.name, RST, padding, item.description + ); + } + if !section.examples.is_empty() { + let _ = writeln!(output, "{BOLD}Examples:{RST}"); + for example in section.examples { + let _ = writeln!(output, " {example}"); + } + } + output +} + +fn render_usage(sections: &[UsageSectionId]) -> String { + let mut usage = String::new(); + for (index, section) in sections.iter().enumerate() { + usage.push_str(&render_section(usage_section(*section))); + if index + 1 < sections.len() { + usage.push('\n'); + } + } + usage +} pub fn help() { debug!("chsr help"); println!("{LONG_ABOUT}"); - println!("{RAR_USAGE_GENERAL}"); + println!( + "{}", + render_usage(&[UsageSectionId::Usage, UsageSectionId::Commands]) + ); } fn rule_to_string(rule: Rule) -> String { @@ -169,7 +645,9 @@ fn rule_to_string(rule: Rule) -> String { Rule::task_id => "task identifier", Rule::command_operations => "cmd", Rule::credentials_operations => "cred", - Rule::cmd_checklisting => "whitelist, blacklist", + Rule::cmd_checklisting | Rule::opt_path_listing | Rule::opt_workdir_listing => { + "whitelist, blacklist" + } Rule::cmd_policy => "allow-all or deny-all", Rule::cmd | Rule::cli => "a command line", Rule::cred_c => "--caps \"cap_net_raw, cap_sys_admin, ...\"", @@ -178,20 +656,32 @@ fn rule_to_string(rule: Rule) -> String { Rule::cred_caps_operations => "caps", Rule::list => "show, list, l", Rule::opt_timeout => "timeout", - Rule::opt_path => "path", + Rule::opt_path | Rule::path => "path", Rule::opt_env => "env", + Rule::opt_workdir => "workdir", Rule::opt_root => "root", Rule::opt_bounding => "bounding", + Rule::opt_skip_auth => "authentication", + Rule::opt_execinfo => "execinfo", + Rule::opt_mask => "umask", Rule::help => "--help", Rule::set => "set", Rule::setpolicy => "setpolicy", Rule::opt_env_listing => "whitelist, blacklist, checklist", + Rule::opt_workdir_args => "setpolicy, set, whitelist, blacklist", Rule::convert => "convert", Rule::convert_type => "json or cbor", Rule::convert_args => "--from, -r, --reconfigure or file_type", Rule::convert_reconfigure => "-r or --reconfigure", Rule::to => "[to_type] [to_file]", Rule::from => "[from_type] [from_file]", + Rule::workdir_policy => "all, none, inherit", + Rule::env_policy => "delete-all, keep-all, inherit", + Rule::path_policy => "delete-all, keep-safe, keep-unsafe, inherit", + Rule::options_operations => "options", + Rule::add => "add", + Rule::del => "del", + Rule::purge => "purge", _ => { println!("{rule:?}"); "unknown rule" @@ -200,67 +690,84 @@ fn rule_to_string(rule: Rule) -> String { .to_string() } -fn usage_concat(usages: &[&'static str]) -> String { - let mut usage = String::new(); - for u in usages { - usage.push_str(u); +const DEFAULT_USAGE_SECTIONS: &[UsageSectionId] = &[ + UsageSectionId::Usage, + UsageSectionId::Commands, + UsageSectionId::Role, + UsageSectionId::Task, + UsageSectionId::Command, + UsageSectionId::Credentials, + UsageSectionId::Convert, +]; + +const OPTIONS_GENERAL_SECTIONS: &[UsageSectionId] = &[UsageSectionId::OptionsGeneral]; +const OPTIONS_AUTH_SECTIONS: &[UsageSectionId] = &[UsageSectionId::OptionsAuth]; +const OPTIONS_EXECINFO_SECTIONS: &[UsageSectionId] = &[UsageSectionId::OptionsExecinfo]; +const OPTIONS_UMASK_SECTIONS: &[UsageSectionId] = &[UsageSectionId::OptionsUmask]; +const OPTIONS_TIMEOUT_SECTIONS: &[UsageSectionId] = &[UsageSectionId::OptionsTimeout]; +const OPTIONS_PATH_SECTIONS: &[UsageSectionId] = &[UsageSectionId::OptionsPath]; +const OPTIONS_WORKDIR_SECTIONS: &[UsageSectionId] = &[UsageSectionId::OptionsWorkdir]; +const OPTIONS_ENV_SECTIONS: &[UsageSectionId] = &[UsageSectionId::OptionsEnv]; +const CREDENTIALS_CAPS_SECTIONS: &[UsageSectionId] = + &[UsageSectionId::CredentialsCaps, UsageSectionId::Listing]; +const COMMAND_LISTING_SECTIONS: &[UsageSectionId] = + &[UsageSectionId::Command, UsageSectionId::Listing]; +const PATH_LISTING_SECTIONS: &[UsageSectionId] = + &[UsageSectionId::OptionsPath, UsageSectionId::Listing]; +const ENV_LISTING_SECTIONS: &[UsageSectionId] = + &[UsageSectionId::OptionsEnv, UsageSectionId::Listing]; +const CONVERT_SECTIONS: &[UsageSectionId] = &[UsageSectionId::Convert]; + +const fn usage_sections_for_rule(rule: Rule) -> Option<&'static [UsageSectionId]> { + match rule { + Rule::options_operations + | Rule::opt_args + | Rule::opt_show + | Rule::opt_show_arg + | Rule::opt_root + | Rule::opt_root_args + | Rule::opt_bounding + | Rule::opt_bounding_args => Some(OPTIONS_GENERAL_SECTIONS), + Rule::opt_skip_auth | Rule::opt_skip_auth_args => Some(OPTIONS_AUTH_SECTIONS), + Rule::opt_execinfo | Rule::opt_execinfo_args => Some(OPTIONS_EXECINFO_SECTIONS), + Rule::opt_mask | Rule::opt_mask_args => Some(OPTIONS_UMASK_SECTIONS), + Rule::opt_timeout + | Rule::opt_timeout_operations + | Rule::opt_timeout_d_arg + | Rule::opt_timeout_t_arg + | Rule::opt_timeout_m_arg => Some(OPTIONS_TIMEOUT_SECTIONS), + Rule::opt_path + | Rule::opt_path_args + | Rule::opt_path_set + | Rule::opt_path_setpolicy + | Rule::path_policy + | Rule::path => Some(OPTIONS_PATH_SECTIONS), + Rule::opt_workdir + | Rule::opt_workdir_args + | Rule::workdir_policy + | Rule::opt_workdir_listing => Some(OPTIONS_WORKDIR_SECTIONS), + Rule::opt_env + | Rule::opt_env_args + | Rule::opt_env_setpolicy + | Rule::env_policy + | Rule::opt_env_set + | Rule::env_key_list + | Rule::env_value_list + | Rule::env_key => Some(OPTIONS_ENV_SECTIONS), + Rule::caps_listing => Some(CREDENTIALS_CAPS_SECTIONS), + Rule::cmd_checklisting => Some(COMMAND_LISTING_SECTIONS), + Rule::opt_path_listing => Some(PATH_LISTING_SECTIONS), + Rule::opt_env_listing => Some(ENV_LISTING_SECTIONS), + Rule::convert => Some(CONVERT_SECTIONS), + _ => None, } - usage } pub fn print_usage(e: pest::error::Error) -> Result> { - let mut usage = usage_concat(&[ - RAR_USAGE_GENERAL, - RAR_USAGE_ROLE, - RAR_USAGE_TASK, - RAR_USAGE_CMD, - RAR_USAGE_CRED, - RAR_CONVERT, - ]); + let mut usage = render_usage(DEFAULT_USAGE_SECTIONS); let e = e.renamed_rules(|rule| { - match rule { - Rule::options_operations - | Rule::opt_args - | Rule::opt_show - | Rule::opt_show_arg - | Rule::opt_path - | Rule::opt_path_args - | Rule::opt_path_set - | Rule::opt_path_setpolicy - | Rule::path_policy - | Rule::path - | Rule::opt_env - | Rule::opt_env_args - | Rule::opt_env_setpolicy - | Rule::env_policy - | Rule::opt_env_set - | Rule::env_key_list - | Rule::env_value_list - | Rule::env_key - | Rule::opt_root - | Rule::opt_root_args - | Rule::opt_bounding - | Rule::opt_bounding_args => { - usage = usage_concat(&[ - RAR_USAGE_OPTIONS_GENERAL, - RAR_USAGE_OPTIONS_PATH, - RAR_USAGE_OPTIONS_ENV, - RAR_USAGE_OPTIONS_TIMEOUT, - ]); - } - Rule::caps_listing => { - usage = usage_concat(&[RAR_USAGE_CRED_CAPS, RAR_USAGE_LISTING]); - } - Rule::cmd_checklisting | Rule::opt_path_listing => { - usage = usage_concat(&[RAR_USAGE_CMD, RAR_USAGE_LISTING]); - } - Rule::opt_env_listing => { - usage = usage_concat(&[RAR_USAGE_OPTIONS_ENV, RAR_USAGE_LISTING]); - } - Rule::convert => { - usage = usage_concat(&[RAR_CONVERT]); - } - _ => {} + if let Some(next_sections) = usage_sections_for_rule(*rule) { + usage = render_usage(next_sections); } rule_to_string(*rule) }); @@ -276,3 +783,37 @@ pub fn print_usage(e: pest::error::Error) -> Result> ); Err(Box::new(e)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_env_listing_to_env_and_listing_sections() { + assert_eq!( + usage_sections_for_rule(Rule::opt_env_listing), + Some(ENV_LISTING_SECTIONS) + ); + } + + #[test] + fn maps_authentication_to_auth_section() { + assert_eq!( + usage_sections_for_rule(Rule::opt_skip_auth), + Some(OPTIONS_AUTH_SECTIONS) + ); + } + + #[test] + fn maps_timeout_rules_to_timeout_section() { + assert_eq!( + usage_sections_for_rule(Rule::opt_timeout_m_arg), + Some(OPTIONS_TIMEOUT_SECTIONS) + ); + } + + #[test] + fn returns_none_for_unknown_rule() { + assert_eq!(usage_sections_for_rule(Rule::EOI), None); + } +} diff --git a/src/chsr/main.rs b/src/chsr/main.rs index 772c19aa..22e96758 100644 --- a/src/chsr/main.rs +++ b/src/chsr/main.rs @@ -7,22 +7,23 @@ mod cli; mod security; mod util; -#[cfg(not(test))] -const ROOTASROLE: &str = env!("RAR_CFG_PATH"); -#[cfg(test)] -const ROOTASROLE: &str = "target/rootasrole.json"; - #[cfg(not(tarpaulin_include))] fn main() -> Result<(), Box> { - use std::{env::temp_dir, fs::OpenOptions}; + use std::env::temp_dir; - use crate::cli::editor::defer; + use crate::{ + cli::editor::defer, + util::{RAR_CFG_DATA_PATH, RAR_CFG_PATH}, + }; use ::landlock::{RestrictionStatus, RulesetStatus}; use capctl::Cap; - use log::{error, warn}; - use rar_common::{LockedSettingsFile, util::definitive_drop}; + use log::{debug, error, warn}; + use rar_common::{ + file::FileSettings, + util::{RAR_CFG_TYPE, definitive_drop}, + }; - use crate::security::{full_program_lock, seccomp_lock}; + use crate::security::full_program_lock; subsribe("chsr")?; // Drop privileges we don't need @@ -40,8 +41,21 @@ fn main() -> Result<(), Box> { let _ = std::fs::remove_dir_all(&folder); }); + let mut settings = FileSettings::write_all(RAR_CFG_PATH, RAR_CFG_DATA_PATH, RAR_CFG_TYPE) + .expect("Error on config read"); + // Apply Landlock restrictions - let ruleset_status = match full_program_lock(&folder) { + let ruleset_status = match full_program_lock( + &folder, + settings + .get_root() + .storage + .settings + .as_ref() + .and_then(|s| s.path.as_ref()) + .and_then(|p| p.to_str()) + .unwrap_or(RAR_CFG_DATA_PATH), + ) { Ok(RestrictionStatus { ruleset, .. }) => ruleset, Err(e) => { warn!("Failed to apply landlock policy: {e:#}"); @@ -49,28 +63,15 @@ fn main() -> Result<(), Box> { } }; - // Then apply seccomp restrictions - seccomp_lock()?; - - let mut settings = LockedSettingsFile::open( - ROOTASROLE, - &OpenOptions::new() - .read(true) - .write(true) - .create(true) - .to_owned(), - true, - ) - .expect("Error on config read"); - - if cli::main(&settings.data.clone(), std::env::args().skip(1)) + if cli::main(&mut settings, std::env::args().skip(1)) .ruleset(ruleset_status) .folder(&folder) .call() .map_err(|e| error!("Unable to edit policy : {e}")) .is_ok_and(|b| b) { - settings.save() + debug!("Saving configuration"); + settings.save_all() } else { Ok(()) } diff --git a/src/chsr/security.rs b/src/chsr/security.rs index 8d55f230..e0ef1760 100644 --- a/src/chsr/security.rs +++ b/src/chsr/security.rs @@ -8,16 +8,17 @@ use landlock::{ }; use libseccomp::{ScmpAction, ScmpFilterContext, ScmpSyscall}; -use crate::{ROOTASROLE, cli::editor::SYSTEM_EDITOR}; +use crate::{cli::editor::SYSTEM_EDITOR_LIST, util::RAR_CFG_PATH}; pub fn full_program_lock( folder: &PathBuf, + rar_cfg_data_path: &str, ) -> Result> { - Ok(Ruleset::default() + let mut ruleset = Ruleset::default() .handle_access(AccessFs::from_all(ABI::V6))? .create()? .add_rule(PathBeneath::new( - PathFd::new(ROOTASROLE)?, + PathFd::new(RAR_CFG_PATH)?, AccessFs::IoctlDev | AccessFs::ReadFile | AccessFs::WriteFile @@ -25,13 +26,25 @@ pub fn full_program_lock( | AccessFs::Refer, ))? .add_rule(PathBeneath::new( - PathFd::new(folder)?, + PathFd::new(rar_cfg_data_path)?, AccessFs::from_all(ABI::V6), ))? .add_rule(PathBeneath::new( - PathFd::new(SYSTEM_EDITOR)?, - AccessFs::from_read(ABI::V6), - ))? + PathFd::new(folder)?, + AccessFs::from_all(ABI::V6), + ))?; + + //TODO: Add rule allowing the path of the policy + for &editor in SYSTEM_EDITOR_LIST { + if !editor.is_empty() { + ruleset = ruleset.add_rule(PathBeneath::new( + PathFd::new(editor)?, + AccessFs::from_read(ABI::V6), + ))?; + } + } + + Ok(ruleset // Allow locale + terminfo .add_rule(PathBeneath::new( PathFd::new("/usr/share/locale")?, @@ -64,86 +77,72 @@ pub fn full_program_lock( .restrict_self()?) } -pub fn seccomp_lock() -> Result<(), Box> { - // Initialize the seccomp filter with the default action to kill the process - let mut ctx = ScmpFilterContext::new_filter(ScmpAction::KillProcess)?; +#[cfg(debug_assertions)] +const SECCOMP: ScmpAction = ScmpAction::Log; +#[cfg(not(debug_assertions))] +const SECCOMP: ScmpAction = ScmpAction::Log; + +/// Applies a seccomp filter that blocks process creation and execution syscalls, +/// as well as some other potentially dangerous syscalls. +/// This was originally to has a allowlist of syscalls, +/// but it turns out that some editors (like vim) use a lot of syscalls, +/// and it's hard to maintain an allowlist without breaking functionality. +pub fn seccomp_lock() -> std::io::Result<()> { + // Allow all by default; explicitly kill process creation/execution. + let mut ctx = ScmpFilterContext::new(ScmpAction::Allow).map_err(|e| { + std::io::Error::other(format!("Failed to create seccomp filter context: {e}")) + })?; - let syscalls = [ - "statx", - "openat", - "geteuid", - "getegid", - "capget", - "capset", - "flock", - "ioctl", - "read", - "write", - "lseek", - "pselect6", - "newfstatat", - "timer_settime", - "fcntl", - "close", - "rt_sigaction", - "rt_sigprocmask", - "mmap", - "getrandom", - "mkdir", - "fstat", - "getuid", - "getgid", - "umask", - "unlink", - "clone3", - "execve", - "munmap", - "wait4", - "brk", - "access", - "pread64", - "arch_prctl", - "set_robust_list", - "rseq", - "mprotect", - "rename", - "exit_group", - "getdents64", - "unlinkat", - "sigaltstack", - "prlimit64", - "getcwd", - "chdir", - "sysinfo", - "readlink", - "fchdir", - "setfsuid", - "setfsgid", - "futex", - "uname", - "getpid", - "chmod", - "fchmod", - "madvise", - "timer_create", - "rt_sigtimedwait", - "set_tid_address", - "clock_nanosleep", - "fsync", - "getxattr", - "setxattr", - "lsetxattr", - "fsetxattr", - "listxattr", - "ftruncate", - "truncate", - "waitid", + let blocked_syscalls = [ + // Blocking forking + "fork", + "vfork", + // Not used by an editor, so they don't need to be allowed. + "ptrace", + "bpf", + "perf_event_open", + "keyctl", + "add_key", + "request_key", + "mount", + "umount2", + "pivot_root", + "setns", + "unshare", + "kexec_load", + "kexec_file_load", + "reboot", + "init_module", + "finit_module", + "delete_module", + "iopl", + "ioperm", + "syslog", + "acct", + "quotactl", + "swapon", + "swapoff", + "userfaultfd", + "io_uring_setup", + "io_uring_enter", + "io_uring_register", + "process_vm_readv", + "process_vm_writev", ]; - for &name in &syscalls { - ctx.add_rule(ScmpAction::Allow, ScmpSyscall::from_name(name)?)?; + for &name in &blocked_syscalls { + ctx.add_rule( + SECCOMP, + ScmpSyscall::from_name(name).map_err(|e| { + std::io::Error::other(format!("Failed to resolve syscall {name}: {e}")) + })?, + ) + .map_err(|e| { + std::io::Error::other(format!("Failed to add seccomp rule for {name}: {e}")) + })?; } - ctx.load()?; + ctx.load() + .map_err(|e| std::io::Error::other(format!("Failed to load seccomp filter: {e}")))?; Ok(()) } diff --git a/src/chsr/util.rs b/src/chsr/util.rs index 6f033439..fe2a630c 100644 --- a/src/chsr/util.rs +++ b/src/chsr/util.rs @@ -1,9 +1,26 @@ use std::mem; +#[cfg(not(debug_assertions))] +use konst::eq_str; use pest::{RuleType, error::LineColLocation}; use rar_common::util::escape_parser_string; +#[cfg(not(test))] +pub const RAR_CFG_PATH: &str = env!("RAR_CFG_PATH"); +#[cfg(test)] +pub const RAR_CFG_PATH: &str = "target/rootasrole.json"; + +#[cfg(not(test))] +pub const RAR_CFG_DATA_PATH: &str = env!("RAR_CFG_DATA_PATH"); +#[cfg(test)] +pub const RAR_CFG_DATA_PATH: &str = "target/rootasrole.json"; + +#[cfg(debug_assertions)] +pub const RAR_CFG_IMMUTABLE: bool = false; +#[cfg(not(debug_assertions))] +pub const RAR_CFG_IMMUTABLE: bool = eq_str(env!("RAR_CFG_IMMUTABLE"), "true"); + const fn start(error: &pest::error::Error) -> (usize, usize) where R: RuleType, diff --git a/src/sr/finder/api/hashchecker.rs b/src/sr/finder/api/hashchecker.rs index 906dabb7..cec3744a 100644 --- a/src/sr/finder/api/hashchecker.rs +++ b/src/sr/finder/api/hashchecker.rs @@ -256,7 +256,7 @@ mod tests { use crate::finder::{ api::hashchecker::{FS_IMMUTABLE_FL, register}, - de::DCommandDeserializer, + de::commands::DCommandDeserializer, }; pub struct Defer(Option); diff --git a/src/sr/finder/api/hierarchy.rs b/src/sr/finder/api/hierarchy.rs index 354d072d..29cca2ae 100644 --- a/src/sr/finder/api/hierarchy.rs +++ b/src/sr/finder/api/hierarchy.rs @@ -1,5 +1,6 @@ use bon::builder; use log::debug; +use rar_common::Cred; use serde_json::Value; use crate::{ @@ -12,8 +13,15 @@ use crate::{ use super::{Api, ApiEvent, EventKey}; fn find_in_parents(event: &mut ApiEvent) -> SrResult<()> { - if let ApiEvent::BestRoleSettingsFound(cli, role, opt_stack, env_path, settings, matching) = - event + if let ApiEvent::BestRoleSettingsFound( + cli, + cred, + role, + opt_stack, + env_path, + settings, + matching, + ) = event { return match role.role().extra_values.get("parents") { Some(Value::Array(parents)) => { @@ -22,6 +30,7 @@ fn find_in_parents(event: &mut ApiEvent) -> SrResult<()> { evaluate_parent_role() .parent(parent.as_ref()) .cli(cli) + .cred(cred) .role(role) .opt_stack(opt_stack) .settings(settings) @@ -34,6 +43,7 @@ fn find_in_parents(event: &mut ApiEvent) -> SrResult<()> { Some(Value::String(parent)) => evaluate_parent_role() .parent(parent.as_ref()) .cli(cli) + .cred(cred) .role(role) .opt_stack(opt_stack) .settings(settings) @@ -54,6 +64,7 @@ fn find_in_parents(event: &mut ApiEvent) -> SrResult<()> { fn evaluate_parent_role<'a>( parent: &str, cli: &Cli, + cred: &Cred, role: &DLinkedRole<'_, 'a>, opt_stack: &mut BorrowedOptStack<'a>, settings: &mut BestExecSettings, @@ -62,7 +73,7 @@ fn evaluate_parent_role<'a>( ) -> SrResult<()> { if let Some(role) = role.config().role(parent) { for task in role.tasks() { - *matching |= settings.task_settings(cli, &task, opt_stack, env_path)?; + *matching |= settings.task_settings(cli, cred, &task, opt_stack, env_path)?; } } Ok(()) diff --git a/src/sr/finder/api/mod.rs b/src/sr/finder/api/mod.rs index 5edae409..85a9ddd6 100644 --- a/src/sr/finder/api/mod.rs +++ b/src/sr/finder/api/mod.rs @@ -1,6 +1,9 @@ use std::{cell::UnsafeCell, collections::HashMap, path::PathBuf}; -use rar_common::database::score::{CmdMin, Score}; +use rar_common::{ + Cred, + database::score::{CmdMin, Score}, +}; use serde_json::Value; use strum::Display; @@ -52,6 +55,7 @@ pub enum ApiEvent<'a, 't, 'c, 'f, 'g, 'h, 'i, 'j, 'k> { ), BestRoleSettingsFound( &'f Cli, + &'k Cred, &'g DLinkedRole<'c, 'a>, &'h mut BorrowedOptStack<'a>, &'k &'k [&'k str], @@ -128,7 +132,7 @@ impl Api { } } -pub(super) fn register_plugins() { +pub fn register_plugins() { #[cfg(feature = "ssd")] ssd::register(); #[cfg(feature = "hashchecker")] diff --git a/src/sr/finder/de.rs b/src/sr/finder/de.rs deleted file mode 100644 index 04f876a8..00000000 --- a/src/sr/finder/de.rs +++ /dev/null @@ -1,2903 +0,0 @@ -/// Lossy deserializer for an optimized user access search -use std::{ - borrow::Cow, collections::HashMap, fmt::Display, ops::Deref, path::PathBuf, str::FromStr, -}; - -use bon::Builder; -use capctl::CapSet; -use derivative::Derivative; -use log::{debug, info}; -use nix::unistd::{Group, User}; -use rar_common::{ - Cred, - database::{ - actor::{DActor, DGroupType, DGroups, DUserType}, - options::Level, - score::{ - ActorMatchMin, CapsMin, CmdMin, Score, SecurityMin, SetgidMin, SetuidMin, TaskScore, - }, - structs::{SCapabilities, SetBehavior}, - }, - util::capabilities_are_exploitable, -}; -use serde::{ - Deserialize, - de::{DeserializeSeed, IgnoredAny, Visitor}, -}; -use serde_json::Value; -use strum::EnumIs; - -use crate::{ - Cli, - finder::{ - api::{Api, ApiEvent}, - cmd, - options::DPathOptions, - }, -}; - -use super::options::Opt; - -#[cfg_attr(test, derive(Builder))] -#[derive(PartialEq, Eq, Debug, Default)] -pub struct DConfigFinder<'a> { - pub options: Option>, - pub roles: Vec>, -} - -#[cfg_attr(test, derive(Builder))] -#[derive(Debug, Derivative)] -#[derivative(PartialEq, Eq)] -pub struct DRoleFinder<'a> { - #[cfg_attr(test, builder(default))] - pub user_min: ActorMatchMin, - #[cfg_attr(test, builder(into))] - pub role: Cow<'a, str>, - #[cfg_attr(test, builder(default))] - pub tasks: Vec>, - pub options: Option>, - #[cfg_attr(test, builder(default))] - pub extra_values: HashMap, Value>, -} - -#[derive(Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] -#[serde(untagged)] -pub enum IdTask<'a> { - Name(#[serde(borrow)] Cow<'a, str>), - Number(usize), -} - -impl Display for IdTask<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - IdTask::Name(name) => write!(f, "{name}"), - IdTask::Number(num) => write!(f, "{num}"), - } - } -} - -#[derive(Debug, Derivative, Builder)] -#[derivative(PartialEq, Eq)] -pub struct DTaskFinder<'a> { - pub id: IdTask<'a>, - #[builder(default)] - pub score: TaskScore, - pub cred: CredData<'a>, - pub commands: Option>, - pub options: Option>, - pub final_path: Option, -} - -#[derive(Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] -#[serde(untagged)] -pub enum DCommand<'a> { - Simple(#[serde(borrow)] Cow<'a, str>), - Complex(Value), -} - -#[cfg(test)] -impl<'a> DCommand<'a> { - pub fn simple(cmd: &'a str) -> Self { - DCommand::Simple(Cow::Borrowed(cmd)) - } - pub fn complex(cmd: Value) -> Self { - DCommand::Complex(cmd) - } -} - -pub struct ConfigFinderDeserializer<'a> { - pub cli: &'a Cli, - pub cred: &'a Cred, - pub env_path: &'a [&'a str], -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for ConfigFinderDeserializer<'a> { - type Value = DConfigFinder<'a>; - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] - #[repr(u8)] - enum Field<'a> { - #[serde(alias = "o")] - Options, - #[serde(alias = "r")] - Roles, - #[serde(untagged, borrow)] - #[allow(dead_code)] - Unknown(Cow<'a, str>), - } - - struct ConfigFinderVisitor<'a> { - cli: &'a Cli, - cred: &'a Cred, - env_path: &'a [&'a str], - human_readable: bool, - } - - impl<'de: 'a, 'a> Visitor<'de> for ConfigFinderVisitor<'a> { - type Value = DConfigFinder<'a>; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("policy") - } - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - let mut options = None; - let mut roles = Vec::new(); - let mut spath = DPathOptions::default_path(); - while let Some(key) = map.next_key()? { - match key { - Field::Options => { - debug!("ConfigFinderVisitor: options"); - let mut opt: Opt = map.next_value()?; - opt.level = Level::Global; - if self.human_readable - && let Some(path) = opt.path.as_ref() - { - spath.union(&path.clone()); - } - options = Some(opt); - } - Field::Roles => { - debug!("ConfigFinderVisitor: roles"); - roles = map.next_value_seed(RoleListFinderDeserializer { - cli: self.cli, - cred: self.cred, - spath: &mut spath, - env_path: self.env_path, - })?; - } - Field::Unknown(_) => { - debug!("ConfigFinderVisitor: unknown"); - let _ = map.next_value::(); - } - } - } - Ok(DConfigFinder { options, roles }) - } - } - const FIELDS: &[&str] = &["options", "roles", "version"]; - let human_readable = deserializer.is_human_readable(); - deserializer.deserialize_struct( - "Config", - FIELDS, - ConfigFinderVisitor { - cli: self.cli, - cred: self.cred, - human_readable, - env_path: self.env_path, - }, - ) - } -} - -struct RoleListFinderDeserializer<'a, 'b> { - cli: &'a Cli, - cred: &'a Cred, - spath: &'b mut DPathOptions<'a>, - env_path: &'a [&'a str], -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for RoleListFinderDeserializer<'a, '_> { - type Value = Vec>; - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct RoleListFinderVisitor<'a, 'b> { - cli: &'a Cli, - cred: &'a Cred, - spath: &'b mut DPathOptions<'a>, - env_path: &'a [&'a str], - } - impl<'de: 'a, 'a> serde::de::Visitor<'de> for RoleListFinderVisitor<'a, '_> { - type Value = Vec>; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("RoleList sequence") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - debug!("RoleListFinderVisitor: visit_seq"); - let mut roles = Vec::new(); - while let Some(role) = seq.next_element_seed(RoleFinderDeserializer { - cli: self.cli, - cred: self.cred, - spath: self.spath, - env_path: self.env_path, - })? { - if let Some(role) = role { - debug!("adding role {role:?}"); - roles.push(role); - } - } - Ok(roles) - } - } - deserializer.deserialize_seq(RoleListFinderVisitor { - cli: self.cli, - cred: self.cred, - spath: self.spath, - env_path: self.env_path, - }) - } -} - -struct RoleFinderDeserializer<'a, 'b> { - cli: &'a Cli, - cred: &'a Cred, - env_path: &'a [&'a str], - spath: &'b mut DPathOptions<'a>, -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for RoleFinderDeserializer<'a, '_> { - type Value = Option>; - #[allow(clippy::too_many_lines)] - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] - #[repr(u8)] - enum Field<'a> { - #[serde(alias = "n")] - Name, - #[serde(alias = "a", alias = "users")] - Actors, - #[serde(alias = "t")] - Tasks, - #[serde(alias = "o")] - Options, - #[serde(untagged, borrow)] - Unknown(Cow<'a, str>), - } - - struct RoleFinderVisitor<'a, 'b> { - cli: &'a Cli, - cred: &'a Cred, - env_path: &'a [&'a str], - spath: &'b mut DPathOptions<'a>, - #[allow(dead_code)] - human_readable: bool, - } - - impl<'de: 'a, 'a> Visitor<'de> for RoleFinderVisitor<'a, '_> { - type Value = Option>; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a role") - } - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - debug!("RoleFinderVisitor: visit_map"); - let mut role = None; - let mut tasks: Vec> = Vec::new(); - let mut options = None; - let mut extra_values = HashMap::new(); - let mut user_min = ActorMatchMin::default(); - while let Some(key) = map.next_key()? { - match key { - Field::Options => { - debug!("RoleFinderVisitor: options"); - let mut opt: Opt = map.next_value()?; - opt.level = Level::Role; - if let Some(path) = opt.path.as_ref() { - self.spath.union(&path.clone()); - } - options = Some(opt); - } - Field::Name => { - debug!("RoleFinderVisitor: name"); - let role_name = map.next_value()?; - if self - .cli - .opt_filter - .as_ref() - .and_then(|x| x.role.as_ref()) - .is_some_and(|r| r != &role_name) - { - while map.next_entry::()?.is_some() {} - return Ok(None); - } - role = Some(role_name); - } - Field::Actors => { - debug!("RoleFinderVisitor: actors"); - user_min = - map.next_value_seed(ActorsFinderDeserializer { cred: self.cred })?; - } - Field::Tasks => { - debug!("RoleFinderVisitor: tasks"); - tasks = map.next_value_seed(TaskListFinderDeserializer { - cli: self.cli, - spath: self.spath, - env_path: self.env_path, - })?; - } - Field::Unknown(key) => { - debug!("RoleFinderVisitor: unknown {key}"); - let unknown: Value = map.next_value()?; - extra_values.insert(key, unknown); - } - } - } - Ok(Some(DRoleFinder { - user_min, - role: role.unwrap_or_default(), - tasks, - options, - extra_values, - })) - } - } - const FIELDS: &[&str] = &["name", "tasks", "options"]; - let human_readable = deserializer.is_human_readable(); - deserializer.deserialize_struct( - "Role", - FIELDS, - RoleFinderVisitor { - cli: self.cli, - cred: self.cred, - spath: self.spath, - env_path: self.env_path, - human_readable, - }, - ) - } -} - -struct ActorsFinderDeserializer<'a> { - cred: &'a Cred, -} - -impl<'de> DeserializeSeed<'de> for ActorsFinderDeserializer<'_> { - type Value = ActorMatchMin; - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct ActorsFinderVisitor<'a> { - cred: &'a Cred, - } - - impl<'de> Visitor<'de> for ActorsFinderVisitor<'_> { - type Value = ActorMatchMin; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a set of users") - } - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let mut user_matches = ActorMatchMin::NoMatch; - while let Some(actor) = seq.next_element::()? { - debug!("ActorsSettingsVisitor: actor {actor:?}"); - let temp = Self::user_matches(self.cred, &actor); - if temp != ActorMatchMin::NoMatch && temp < user_matches { - info!("ActorsSettingsVisitor: Better actor found {temp:?}"); - user_matches = temp; - } - } - Ok(user_matches) - } - } - - impl ActorsFinderVisitor<'_> { - fn match_groups(groups: &[Group], role_groups: &[&DGroups<'_>]) -> bool { - for role_group in role_groups { - if match role_group { - DGroups::Single(group) => groups.iter().any(|g| group == g), - DGroups::Multiple(multiple_actors) => multiple_actors - .iter() - .all(|actor| groups.iter().any(|g| actor == g)), - } { - return true; - } - } - false - } - fn user_matches(user: &Cred, actor: &DActor<'_>) -> ActorMatchMin { - match actor { - DActor::User { id, .. } => { - if *id == user.user { - return ActorMatchMin::UserMatch; - } - } - DActor::Group { groups, .. } => { - if Self::match_groups(&user.groups, &[groups]) { - return ActorMatchMin::GroupMatch(groups.len()); - } - } - DActor::Unknown(element) => { - unimplemented!("Unknown actor type: {:?}", element); - } - } - ActorMatchMin::NoMatch - } - } - - deserializer.deserialize_seq(ActorsFinderVisitor { cred: self.cred }) - } -} - -struct TaskListFinderDeserializer<'a, 'b> { - cli: &'a Cli, - env_path: &'a [&'a str], - spath: &'b mut DPathOptions<'a>, -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for TaskListFinderDeserializer<'a, '_> { - type Value = Vec>; - - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct TaskListFinderVisitor<'a, 'b> { - cli: &'a Cli, - spath: &'b mut DPathOptions<'a>, - env_path: &'a [&'a str], - } - impl<'de: 'a, 'a> serde::de::Visitor<'de> for TaskListFinderVisitor<'a, '_> { - type Value = Vec>; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("TaskList sequence") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let mut tasks = Vec::new(); - let mut i = 0; - while let Some(element) = seq.next_element_seed(TaskFinderDeserializer { - cli: self.cli, - spath: self.spath, - env_path: self.env_path, - i, - })? { - if let Some(task) = element { - debug!("adding task {task:?}"); - tasks.push(task); - i += 1; - } - } - Ok(tasks) - } - } - deserializer.deserialize_seq(TaskListFinderVisitor { - cli: self.cli, - spath: self.spath, - env_path: self.env_path, - }) - } -} - -struct TaskFinderDeserializer<'a, 'b> { - cli: &'a Cli, - i: usize, - env_path: &'a [&'a str], - spath: &'b mut DPathOptions<'a>, -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for TaskFinderDeserializer<'a, '_> { - type Value = Option>; - - #[allow(clippy::too_many_lines)] - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] - #[repr(u8)] - enum Field<'a> { - #[serde(alias = "n")] - Name, - #[serde(alias = "i", alias = "credentials")] - Cred, - #[serde(alias = "c", alias = "cmds")] - Commands, - #[serde(alias = "o")] - Options, - #[serde(untagged, borrow)] - Unknown(Cow<'a, str>), - } - - struct TaskFinderVisitor<'a, 'b> { - cli: &'a Cli, - i: usize, - env_path: &'a [&'a str], - spath: &'b mut DPathOptions<'a>, - human_readable: bool, - } - - impl<'de: 'a, 'a> serde::de::Visitor<'de> for TaskFinderVisitor<'a, '_> { - type Value = Option>; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("STask structure") - } - - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - // Use local temporaries for each field - let mut id = IdTask::Number(self.i); - let mut score = TaskScore::default(); - let mut commands = None; - let mut options = None; - let mut final_path = None; - //let mut extra_values = HashMap::new(); - let mut cred = CredData::default(); - - while let Some(key) = map.next_key()? { - match key { - Field::Options => { - debug!("TaskFinderVisitor: options"); - let mut opt: Opt = map.next_value()?; - opt.level = Level::Task; - if let Some(path) = opt.path.as_ref() { - self.spath.union(&path.clone()); - } - if self.cli.info && opt.execinfo.is_some_and(|i| i.is_hide()) { - while map.next_entry::()?.is_some() {} - return Ok(None); - } - // skip the task if env_override is required and not allowed - if self.cli.opt_filter.as_ref().is_some_and(|o| { - // we have a filter - o.env_behavior.as_ref().is_some_and(|_| { - // the filter overrides env behavior - opt.env.as_ref().is_some_and(|e| { - // the task specifies env options - e.override_behavior.is_some_and(|b| !b) // the task specifies override behavior and deny it - }) - }) - // in any other case, we cannot know if this task is valid or not (as we don't know the inherited env override value) - }) { - while map.next_entry::()?.is_some() {} - return Ok(None); - } - options = Some(opt); - } - Field::Name => { - debug!("TaskFinderVisitor: name"); - let task_name = map.next_value()?; - if self - .cli - .opt_filter - .as_ref() - .and_then(|x| x.task.as_ref()) - .is_some_and(|t| IdTask::Name(t.into()) != task_name) - { - while map.next_entry::()?.is_some() {} - return Ok(None); - } - id = task_name; - } - Field::Cred => { - debug!("TaskFinderVisitor: cred"); - let result = map - .next_value_seed(CredFinderDeserializerReturn { cli: self.cli })?; - if !result.ok { - while map.next_entry::()?.is_some() {} - return Ok(None); - } - cred = result.cred; - score.setuser_min = result.score.setuser_min; - score.caps_min = result.score.caps_min; - } - Field::Commands => { - debug!("TaskFinderVisitor: commands"); - // if is_human_readable -> next_value - // else -> next_value_seed -> no memory allocation, just the result, thus highly optimizing - if self.human_readable { - commands = Some(map.next_value()?); - } else { - map.next_value_seed(DCommandListDeserializer { - env_path: &self.spath.calc_path(self.env_path), - cmd_path: &self.cli.cmd_path, - cmd_args: &self.cli.cmd_args, - final_path: &mut final_path, - cmd_min: &mut score.cmd_min, - blocker: false, - })?; - } - } - Field::Unknown(_key) => { - debug!("TaskFinderVisitor: unknown"); - let _ = map.next_value::()?; - } - } - } - debug!("TaskFinderVisitor: final_path {final_path:?}"); - Ok(Some(DTaskFinder { - id, - score, - cred, - commands, - options, - final_path, - })) - } - } - - const FIELDS: &[&str] = &["name", "cred", "commands", "options"]; - let human_readable = deserializer.is_human_readable(); - deserializer.deserialize_struct( - "STask", - FIELDS, - TaskFinderVisitor { - i: self.i, - cli: self.cli, - env_path: self.env_path, - spath: self.spath, - human_readable, - }, - ) - } -} - -struct CredFinderDeserializerReturn<'a> { - cli: &'a Cli, -} - -#[derive(Debug, PartialEq, Eq, Default, Builder)] -pub struct CredData<'a> { - pub setuid: Option>, - pub setgroups: Option>, - pub caps: Option, - #[builder(default)] - pub extra_values: HashMap, Value>, -} - -#[derive(Debug, PartialEq, Eq, Default, Clone, Builder)] -pub struct CredOwnedData { - pub setuid: Option, - pub setgroups: Option>, - pub caps: Option, - #[builder(default)] - pub extra_values: HashMap, -} - -#[derive(Debug)] -struct CredResult<'a> { - cred: CredData<'a>, - score: TaskScore, - ok: bool, -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for CredFinderDeserializerReturn<'a> { - type Value = CredResult<'a>; - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] - enum Field<'a> { - #[serde(alias = "u")] - Setuid, - #[serde(alias = "g", alias = "setgroups")] - Setgid, - #[serde(alias = "c", alias = "capabilities")] - Caps, - #[serde(untagged, borrow)] - Other(Cow<'a, str>), - } - - struct CredFinderVisitor<'a> { - cli: &'a Cli, - } - - fn get_caps_min(caps: CapSet) -> CapsMin { - if caps.is_empty() { - CapsMin::NoCaps - } else if caps == !CapSet::empty() { - CapsMin::CapsAll - } else if capabilities_are_exploitable(caps) { - CapsMin::CapsAdmin(caps.size()) - } else { - CapsMin::CapsNoAdmin(caps.size()) - } - } - - impl<'de: 'a, 'a> serde::de::Visitor<'de> for CredFinderVisitor<'a> { - type Value = CredResult<'a>; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("Cred structure") - } - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - let mut setuid = None; - let mut setgroups = None; - let mut caps = None; - let mut score = TaskScore::default(); - let mut ok = true; - let mut extra_values = HashMap::new(); - while let Some(key) = map.next_key()? { - match key { - Field::Setuid => { - debug!("CredFinderVisitor: setuid"); - let (user, setuser_min, user_ok) = - map.next_value_seed(SetUserDeserializerReturn { cli: self.cli })?; - setuid = user; - score.setuser_min.uid = setuser_min; - if !user_ok { - ok = false; - } - } - Field::Setgid => { - debug!("CredFinderVisitor: setgid"); - let (groups, setuser_min, groups_ok) = - map.next_value_seed(SetGroupsDeserializerReturn { cli: self.cli })?; - setgroups = groups; - score.setuser_min.gid = setuser_min; - if !groups_ok { - ok = false; - } - } - Field::Caps => { - debug!("CredFinderVisitor: capabilities"); - let scaps: SCapabilities = map.next_value()?; - let capset = scaps.to_capset(); - score.caps_min = get_caps_min(capset); - caps = Some(capset); - } - Field::Other(n) => { - debug!("CredFinderVisitor: unknown {n}"); - let v: Value = map.next_value()?; - extra_values.insert(n, v); - } - } - } - debug!("CredFinderVisitor: end"); - Ok(CredResult { - cred: CredData { - setuid, - setgroups, - caps, - extra_values, - }, - score, - ok, - }) - } - } - const FIELDS: &[&str] = &["setuid", "setgroups", "capabilities", "0", "1", "2"]; - deserializer.deserialize_struct("Cred", FIELDS, CredFinderVisitor { cli: self.cli }) - } -} - -// New deserializer for SetGroups that returns values instead of using &mut -struct SetGroupsDeserializerReturn<'a> { - cli: &'a Cli, -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for SetGroupsDeserializerReturn<'a> { - type Value = (Option>, Option, bool); - #[allow(clippy::too_many_lines)] - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] - #[repr(u8)] - enum Field { - #[serde(alias = "d")] - Default, - #[serde(alias = "f")] - Fallback, - #[serde(alias = "a")] - Add, - #[serde(alias = "s", alias = "sub")] - Del, - } - struct SGroupsChooserVisitor<'a> { - cli: &'a Cli, - } - impl<'de: 'a, 'a> serde::de::Visitor<'de> for SGroupsChooserVisitor<'a> { - type Value = (Option>, Option, bool); - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("SGroups structure") - } - fn visit_borrowed_str(self, v: &'de str) -> Result - where - E: serde::de::Error, - { - debug!("SGroupsChooserVisitor: visit_borrowed_str"); - let group: DGroupType<'_> = v - .parse::() - .map_or_else(|_| v.into(), std::convert::Into::into); - let score = Some(SetgidMin::from(&group)); - let ok = true; - if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()) - && y.len() == 1 - && y[0] - != group - .fetch_id() - .ok_or_else(|| serde::de::Error::custom("Group does not exist"))? - { - return Ok((None, None, false)); - } - Ok((Some(DGroups::Single(group)), score, ok)) - } - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - debug!("SGroupsChooserVisitor: visit_str"); - self.visit_string(v.to_string()) - } - fn visit_string(self, v: String) -> Result - where - E: serde::de::Error, - { - debug!("SGroupsChooserVisitor: visit_string"); - let group: DGroupType<'_> = v - .parse::() - .map_or_else(|_| Cow::::from(v).into(), std::convert::Into::into); - let score = Some(SetgidMin::from(&group)); - let ok = true; - if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()) - && y.len() == 1 - && y[0] - != group - .fetch_id() - .ok_or_else(|| serde::de::Error::custom("Group does not exist"))? - { - return Ok((None, None, false)); - } - Ok((Some(DGroups::Single(group)), score, ok)) - } - - fn visit_u64(self, v: u64) -> Result - where - E: serde::de::Error, - { - debug!("SGroupsChooserVisitor: visit_u64"); - let group: DGroupType<'_> = >::from( - u32::try_from(v).map_err(|_| serde::de::Error::custom("Group id too large"))?, - ); - let score = Some(SetgidMin::from(&group)); - let ok = true; - if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()) - && y.len() == 1 - && y[0] - != group - .fetch_id() - .ok_or_else(|| serde::de::Error::custom("Group does not exist"))? - { - return Ok((None, None, false)); - } - Ok((Some(DGroups::Single(group)), score, ok)) - } - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - debug!("SGroupsChooserVisitor: visit_seq"); - let mut groups = None; - let mut score = None; - let mut ok = false; - let filter = self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()); - while let Some(group) = seq.next_element::()? { - if let Some(u) = filter { - let parsed_ids: Vec = - (&group).try_into().map_err(serde::de::Error::custom)?; - if *u == parsed_ids { - ok = true; - groups = Some(group.clone()); - score.replace((&group).into()); - while seq.next_element::()?.is_some() {} - break; - } - } else { - groups = Some(group.clone()); - ok = true; - score.replace((&group).into()); - } - } - Ok((groups, score, ok)) - } - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - let mut groups = None; - let mut score = None; - let mut ok = false; - let filter = self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()); - 'fields: while let Some(key) = map.next_key()? { - match key { - Field::Default => { - debug!("SGroupsChooserVisitor: default"); - let default = map.next_value::()?; - if default.is_all() { - ok = true; - } - } - Field::Fallback => { - debug!("SGroupsChooserVisitor: fallback"); - let value = map.next_value::()?; - if let Some(u) = filter { - let parsed_ids: Vec = - (&value).try_into().map_err(serde::de::Error::custom)?; - if *u == parsed_ids { - ok = true; - groups = Some(value.clone()); - score.replace((&value).into()); - } - } else { - groups = Some(value.clone()); - ok = true; - score.replace((&value).into()); - } - } - Field::Add => { - debug!("SGroupsChooserVisitor: add"); - if let Some(filter) = filter { - let add = map.next_value::>()?; - for group in add.iter() { - let v: Vec = - group.try_into().map_err(serde::de::Error::custom)?; - if v == *filter { - ok = true; - groups = Some(group.to_owned()); - score.replace(group.into()); - while map.next_entry::()?.is_some() - { - } - break; - } - } - } else { - map.next_value::()?; - } - } - Field::Del => { - debug!("SGroupsChooserVisitor: del"); - if let Some(u) = filter { - for group in map.next_value::>()?.iter() { - if let Ok(v) = TryInto::>::try_into(group) { - if v == *u { - while map - .next_entry::()? - .is_some() - { - } - ok = false; - groups = None; - score = None; - break 'fields; - } - } else { - return Err(serde::de::Error::custom("Invalid group")); - } - } - } else { - map.next_value::()?; - } - } - } - } - Ok((groups, score, ok)) - } - } - deserializer.deserialize_any(SGroupsChooserVisitor { cli: self.cli }) - } -} - -// New deserializer for SetUser that returns values instead of using &mut -struct SetUserDeserializerReturn<'a> { - cli: &'a Cli, -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for SetUserDeserializerReturn<'a> { - type Value = (Option>, Option, bool); - #[allow(clippy::too_many_lines)] - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] - #[repr(u8)] - enum Field { - #[serde(alias = "d")] - Default, - #[serde(alias = "f")] - Fallback, - #[serde(alias = "a")] - Add, - #[serde(alias = "s", alias = "sub")] - Del, - } - struct SetUserVisitor<'a> { - cli: &'a Cli, - } - impl<'de: 'a, 'a> serde::de::Visitor<'de> for SetUserVisitor<'a> { - type Value = (Option>, Option, bool); - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("SUser structure") - } - fn visit_borrowed_str(self, v: &'de str) -> Result - where - E: serde::de::Error, - { - debug!("SetUserVisitor: visit_borrowed_str"); - let user = v - .parse::() - .map_or_else(|_| DUserType::from(v), DUserType::from); - let score = Some(SetuidMin::from(&user)); - let ok = true; - if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.user) - && *y - != user - .fetch_id() - .ok_or_else(|| serde::de::Error::custom("User does not exist"))? - { - return Ok((None, None, false)); - } - Ok((Some(user), score, ok)) - } - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - debug!("SetUserVisitor: visit_str"); - self.visit_string(v.to_string()) - } - fn visit_string(self, v: String) -> Result - where - E: serde::de::Error, - { - debug!("SetUserVisitor: visit_string"); - let user = v - .parse::() - .map_or_else(|_| DUserType::from(v), DUserType::from); - let score = Some(SetuidMin::from(&user)); - let ok = true; - if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.user) - && *y - != user - .fetch_id() - .ok_or_else(|| serde::de::Error::custom("User does not exist"))? - { - return Ok((None, None, false)); - } - Ok((Some(user), score, ok)) - } - fn visit_u64(self, v: u64) -> Result - where - E: serde::de::Error, - { - debug!("SetUserVisitor: visit_i64"); - let user = DUserType::from( - u32::try_from(v).map_err(|_| serde::de::Error::custom("User id too large"))?, - ); - let score = Some(SetuidMin::from(&user)); - let ok = true; - if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.user) - && *y - != user - .fetch_id() - .ok_or_else(|| serde::de::Error::custom("User does not exist"))? - { - return Ok((None, None, false)); - } - Ok((Some(user), score, ok)) - } - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - let mut user = None; - let mut score = None; - let mut ok = false; - let filter = self.cli.opt_filter.as_ref().and_then(|x| x.user.as_ref()); - 'fields: while let Some(key) = map.next_key()? { - match key { - Field::Default => { - debug!("SUserChooserVisitor: default"); - let default = map.next_value::()?; - if default.is_all() { - ok = true; - } - } - Field::Fallback => { - debug!("SUserChooserVisitor: fallback"); - let value = map.next_value::()?; - if let Some(u) = filter { - let userid = value.fetch_id().ok_or_else(|| { - serde::de::Error::custom("User does not exist") - })?; - if u == &userid { - score.replace((&value).into()); - user = Some(value); - ok = true; - } - } else { - ok = true; - score.replace((&value).into()); - user = Some(value); - } - } - Field::Add => { - debug!("SUserChooserVisitor: add"); - if let Some(filter) = filter { - let users = map.next_value::>()?; - for user_item in users.iter() { - let user_id = user_item.fetch_id().ok_or_else(|| { - serde::de::Error::custom("User does not exist") - })?; - if user_id == *filter { - ok = true; - user = Some(user_item.to_owned()); - score.replace(user_item.into()); - break; - } - } - } else { - map.next_value::()?; - } - } - Field::Del => { - debug!("SUserChooserVisitor: del"); - if let Some(u) = filter { - let users = map.next_value::>()?; - for user_item in users.iter() { - let user_id = user_item.fetch_id().ok_or_else(|| { - serde::de::Error::custom("User does not exist") - })?; - if user_id == *u { - while map.next_entry::()?.is_some() - { - } - score = None; - user = None; - ok = false; - break 'fields; - } - } - } else { - map.next_value::()?; - } - } - } - } - Ok((user, score, ok)) - } - } - deserializer.deserialize_any(SetUserVisitor { cli: self.cli }) - } -} - -/// This struct keeps the list of commands because options may be written after -#[cfg_attr(test, derive(Builder))] -#[derive(PartialEq, Eq, Debug)] -pub struct DCommandList<'a> { - #[cfg_attr(test, builder(start_fn, into))] - pub default_behavior: Option, - #[cfg_attr(test, builder(default, into))] - pub add: Cow<'a, [DCommand<'a>]>, - #[cfg_attr(test, builder(default, into))] - pub del: Cow<'a, [DCommand<'a>]>, -} - -impl<'de: 'a, 'a> Deserialize<'de> for DCommandList<'a> { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] - #[repr(u8)] - enum Field { - #[serde(alias = "d", alias = "default_behavior")] - Default, - #[serde(alias = "a")] - Add, - #[serde(alias = "s", alias = "sub", alias = "del")] - Del, - } - #[derive(Default)] - struct DCommandListVisitor<'a> { - _phantom: std::marker::PhantomData<&'a ()>, - } - impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandListVisitor<'a> { - type Value = DCommandList<'a>; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("CommandList structure") - } - - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - let mut default_behavior = None; - let mut add: Cow<'_, [DCommand<'_>]> = Cow::Borrowed(&[]); - let mut del: Cow<'_, [DCommand<'_>]> = Cow::Borrowed(&[]); - while let Some(key) = map.next_key()? { - match key { - Field::Default => { - debug!("DCommandListVisitor: default"); - default_behavior = Some(map.next_value()?); - } - Field::Add => { - debug!("DCommandListVisitor: add"); - add = map.next_value()?; - } - Field::Del => { - debug!("DCommandListVisitor: del"); - del = map.next_value()?; - } - } - } - Ok(DCommandList { - default_behavior, - add, - del, - }) - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let mut add = Vec::new(); - while let Some(command) = seq.next_element()? { - add.push(command); - } - Ok(DCommandList { - default_behavior: None, - add: Cow::Owned(add), - del: Cow::Borrowed(&[]), - }) - } - fn visit_string(self, v: String) -> Result - where - E: serde::de::Error, - { - self.visit_str(&v) - } - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - let set = SetBehavior::from_str(v).map_err(serde::de::Error::custom)?; - Ok(DCommandList { - default_behavior: Some(set), - add: Cow::Borrowed(&[]), - del: Cow::Borrowed(&[]), - }) - } - fn visit_borrowed_str(self, v: &'de str) -> Result - where - E: serde::de::Error, - { - self.visit_str(v) - } - } - deserializer.deserialize_any(DCommandListVisitor::default()) - } -} - -/// This struct evaluates commands directly from deserialization -pub struct DCommandListDeserializer<'a> { - env_path: &'a [&'a str], - cmd_path: &'a PathBuf, - cmd_args: &'a [String], - pub final_path: &'a mut Option, - pub cmd_min: &'a mut CmdMin, - pub blocker: bool, -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for DCommandListDeserializer<'a> { - type Value = bool; - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_any(self) - } -} - -impl DCommandListDeserializer<'_> { - const fn generate_dcommand_deserializer(&mut self) -> DCommandDeserializer<'_> { - DCommandDeserializer { - env_path: self.env_path, - cmd_path: self.cmd_path, - cmd_args: self.cmd_args, - final_path: self.final_path, - cmd_min: self.cmd_min, - } - } -} - -impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandListDeserializer<'a> { - type Value = bool; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("CommandList Deserializer structure") - } - - fn visit_seq(mut self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let mut result = false; - while let Some(bool) = seq.next_element_seed(self.generate_dcommand_deserializer())? { - if bool && self.blocker { - return Ok(true); - } - result |= bool; - } - Ok(result) - } - - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - #[derive(Deserialize)] - #[serde(field_identifier, rename_all = "lowercase")] - #[repr(u8)] - enum Field { - #[serde(alias = "d", alias = "default_behavior")] - Default, - #[serde(alias = "a")] - Add, - #[serde(alias = "s", alias = "sub")] - Del, - } - let mut result = false; - let mut default = SetBehavior::None; - while let Some(key) = map.next_key()? { - match key { - Field::Default => { - debug!("DCommandListVisitor: default"); - default = map.next_value()?; - } - Field::Del => { - let deserializer = DCommandListDeserializer { - env_path: self.env_path, - cmd_path: self.cmd_path, - cmd_args: self.cmd_args, - final_path: self.final_path, - cmd_min: self.cmd_min, - blocker: true, - }; - let res = map.next_value_seed(deserializer)?; - if res { - while map.next_entry::()?.is_some() {} - return Ok(false); - } - } - Field::Add => { - if default.is_all() { - let _ = map.next_value::(); - } else { - let deserializer = DCommandListDeserializer { - env_path: self.env_path, - cmd_path: self.cmd_path, - cmd_args: self.cmd_args, - final_path: self.final_path, - cmd_min: self.cmd_min, - blocker: false, - }; - result |= map.next_value_seed(deserializer)?; - } - } - } - } - Ok(result) - } - fn visit_string(self, v: String) -> Result - where - E: serde::de::Error, - { - self.visit_str(&v) - } - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - let set = SetBehavior::from_str(v).map_err(serde::de::Error::custom)?; - Ok(set.is_all()) - } - fn visit_borrowed_str(self, v: &'de str) -> Result - where - E: serde::de::Error, - { - self.visit_str(v) - } -} - -pub(super) struct DCommandDeserializer<'a> { - pub(super) env_path: &'a [&'a str], - pub(super) cmd_path: &'a PathBuf, - pub(super) cmd_args: &'a [String], - pub(super) final_path: &'a mut Option, - pub(super) cmd_min: &'a mut CmdMin, -} - -impl<'de: 'a, 'a> DeserializeSeed<'de> for DCommandDeserializer<'a> { - type Value = bool; - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct DCommandVisitor<'a> { - env_path: &'a [&'a str], - cmd_path: &'a PathBuf, - cmd_args: &'a [String], - final_path: &'a mut Option, - cmd_min: &'a mut CmdMin, - } - impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandVisitor<'a> { - type Value = bool; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("Command structure") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - let mut final_path = None; - let mut result = false; - debug!("DCommandVisitor: command {v}"); - let cmd_min = cmd::evaluate_command_match( - self.env_path, - self.cmd_path, - self.cmd_args, - v, - *self.cmd_min, - &mut final_path, - ); - debug!("DCommandVisitor: command result {cmd_min:?}"); - if cmd_min.better(*self.cmd_min) { - debug!("DCommandVisitor: better command found"); - result = true; - *self.final_path = final_path; - *self.cmd_min = cmd_min; - } - Ok(result) - } - - fn visit_map(self, mut map: V) -> Result - where - V: serde::de::MapAccess<'de>, - { - let mut map_value = Vec::new(); - while let Some((key, value)) = map.next_entry::<&str, Value>()? { - map_value.push((key, value)); - } - Api::notify(ApiEvent::ProcessComplexCommand( - &Value::Object( - map_value - .into_iter() - .map(|(k, v)| (k.to_string(), v)) - .collect(), - ), - self.env_path, - self.cmd_path, - self.cmd_args, - self.cmd_min, - self.final_path, - )) - .map(|()| true) - .map_err(|_| serde::de::Error::custom("Failed to notify process complex command")) - } - } - deserializer.deserialize_any(DCommandVisitor { - env_path: self.env_path, - cmd_path: self.cmd_path, - cmd_args: self.cmd_args, - final_path: self.final_path, - cmd_min: self.cmd_min, - }) - } -} - -impl<'a> DConfigFinder<'a> { - pub fn roles<'s>(&'s self) -> impl Iterator> { - self.roles.iter().map(|role| DLinkedRole::new(self, role)) - } - - #[cfg(any(feature = "hierarchy", feature = "ssd"))] - pub fn role<'s>(&'s self, role_name: &str) -> Option> { - self.roles - .iter() - .find(|r| r.role == role_name) - .map(|role| DLinkedRole::new(self, role)) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct DLinkedRole<'c, 'a> { - parent: &'c DConfigFinder<'a>, - role: &'c DRoleFinder<'a>, -} - -impl<'c, 'a> DLinkedRole<'c, 'a> { - const fn new(parent: &'c DConfigFinder<'a>, role: &'c DRoleFinder<'a>) -> Self { - Self { parent, role } - } - - pub fn tasks<'t>(&'t self) -> impl Iterator> { - self.role - .tasks - .iter() - .map(|task| DLinkedTask::new(self, task)) - } - - pub const fn role(&self) -> &DRoleFinder<'a> { - self.role - } - - pub const fn config(&self) -> &DConfigFinder<'a> { - self.parent - } -} - -#[derive(Clone, Copy, Debug)] -pub struct DLinkedTask<'t, 'c, 'a> { - parent: &'t DLinkedRole<'c, 'a>, - pub task: &'t DTaskFinder<'a>, -} - -impl<'t, 'c, 'a> DLinkedTask<'t, 'c, 'a> { - const fn new(parent: &'t DLinkedRole<'c, 'a>, task: &'t DTaskFinder<'a>) -> Self { - Self { parent, task } - } - - pub fn commands<'l>(&'l self) -> Option> { - self.task - .commands - .as_ref() - .map(|list| DLinkedCommandList::new(self, list)) - } - - pub const fn role(&self) -> &DLinkedRole<'c, 'a> { - self.parent - } - - pub const fn task(&self) -> &DTaskFinder<'a> { - self.task - } - - pub fn score(&self, cmd_min: CmdMin, security_min: SecurityMin) -> Score { - Score::builder() - .user_min(self.role().role.user_min) - .caps_min(self.score.caps_min) - .cmd_min(cmd_min) - .security_min(security_min) - .setuser_min(self.score.setuser_min) - .build() - } -} - -impl<'a> Deref for DLinkedTask<'_, '_, 'a> { - type Target = DTaskFinder<'a>; - fn deref(&self) -> &Self::Target { - self.task - } -} - -pub struct DLinkedCommandList<'l, 't, 'c, 'a> { - #[allow(dead_code)] // TODO: remove this - parent: &'l DLinkedTask<'t, 'c, 'a>, - command_list: &'l DCommandList<'a>, -} - -impl<'l, 't, 'c, 'a> DLinkedCommandList<'l, 't, 'c, 'a> { - const fn new(parent: &'l DLinkedTask<'t, 'c, 'a>, list: &'l DCommandList<'a>) -> Self { - Self { - parent, - command_list: list, - } - } - - pub fn add<'d>(&'d self) -> impl Iterator> { - self.command_list - .add - .iter() - .map(|cmd| DLinkedCommand::new(self, cmd)) - } - - pub fn del<'d>(&'d self) -> impl Iterator> { - self.command_list - .del - .iter() - .map(|cmd| DLinkedCommand::new(self, cmd)) - } -} - -impl<'a> Deref for DLinkedCommandList<'_, '_, '_, 'a> { - type Target = DCommandList<'a>; - fn deref(&self) -> &Self::Target { - self.command_list - } -} - -pub struct DLinkedCommand<'d, 'l, 't, 'c, 'a> { - #[allow(dead_code)] // TODO: remove this - parent: &'d DLinkedCommandList<'l, 't, 'c, 'a>, - pub command: &'d DCommand<'a>, -} - -impl<'d, 'l, 't, 'c, 'a> DLinkedCommand<'d, 'l, 't, 'c, 'a> { - const fn new( - parent: &'d DLinkedCommandList<'l, 't, 'c, 'a>, - command: &'d DCommand<'a>, - ) -> Self { - Self { parent, command } - } - - #[allow(dead_code)] // TODO: remove this - pub const fn task(&self) -> &DLinkedTask<'t, 'c, 'a> { - self.parent.parent - } -} - -impl<'a> Deref for DLinkedCommand<'_, '_, '_, '_, 'a> { - type Target = DCommand<'a>; - fn deref(&self) -> &Self::Target { - self.command - } -} - -#[cfg(test)] -mod tests { - - use std::fs; - - use super::*; - use capctl::Cap; - use cbor4ii::core::utils::SliceReader; - use nix::unistd::{getgid, getuid}; - use rar_common::database::{ - FilterMatcher, - actor::{DGroupType, SGroupType, SGroups}, - score::{SetUserMin, SetgidMin, SetuidMin}, - }; - use test_log::test; - - fn get_non_root_uid(nth: usize) -> Option { - // list all users - let passwd = fs::read_to_string("/etc/passwd").unwrap(); - let passwd: Vec<&str> = passwd.split('\n').collect(); - passwd - .iter() - .map(|line| { - let line: Vec<&str> = line.split(':').collect(); - line[2].parse::().unwrap() - }) - .filter(|uid| *uid != 0) - .nth(nth) - } - - fn get_non_root_gid(nth: usize) -> Option { - // list all users - let passwd = fs::read_to_string("/etc/group").unwrap(); - let passwd: Vec<&str> = passwd.split('\n').collect(); - passwd - .iter() - .map(|line| { - let line: Vec<&str> = line.split(':').collect(); - line[2].parse::().unwrap() - }) - .filter(|uid| *uid != 0) - .nth(nth) - } - - fn convert_json_to_cbor(json: &str) -> Vec { - let value: Value = serde_json::from_str(json).unwrap(); - - cbor4ii::serde::to_vec(Vec::new(), &value).unwrap() - } - - #[test] - fn test_idtask_display() { - let name = IdTask::Name(Cow::Borrowed("test")); - let number = IdTask::Number(42); - assert_eq!(format!("{name}"), "test"); - assert_eq!(format!("{number}"), "42"); - } - - #[test] - fn test_dcommandlist_deserialize_seq() { - let json = r#"["ls", "cat"]"#; - let list: DCommandList = serde_json::from_str(json).unwrap(); - assert_eq!(list.add.len(), 2); - assert!(matches!(list.add[0], DCommand::Simple(_))); - } - - #[test] - fn test_dcommandlist_deserialize_map() { - let json = r#"{"default": "all", "add": ["ls"], "del": ["rm"]}"#; - let list: DCommandList = serde_json::from_str(json).unwrap(); - assert_eq!(list.default_behavior.unwrap(), SetBehavior::All); - assert_eq!(list.add.len(), 1); - assert_eq!(list.del.len(), 1); - } - - #[test] - fn test_dcommandlist_deserialize_all_or_none() { - let json = "\"all\""; - let list: DCommandList = serde_json::from_str(json).unwrap(); - assert_eq!(list.default_behavior, Some(SetBehavior::All)); - assert_eq!(list.add.len(), 0); - assert_eq!(list.del.len(), 0); - let json = "\"none\""; - let list: DCommandList = serde_json::from_str(json).unwrap(); - assert_eq!(list.default_behavior, Some(SetBehavior::None)); - assert_eq!(list.add.len(), 0); - assert_eq!(list.del.len(), 0); - } - - #[test] - fn test_dcommandlist_deserialize_empty() { - let json = "{}"; - let list: DCommandList = serde_json::from_str(json).unwrap(); - assert_eq!(list.default_behavior, None); - assert_eq!(list.add.len(), 0); - assert_eq!(list.del.len(), 0); - } - - #[test] - fn test_dcommandlist_deserialize_invalid() { - let json = r#"{"default": "invalid", "add": ["ls"], "del": ["rm"]}"#; - let result: Result = serde_json::from_str(json); - assert!(result.is_err()); - } - - #[test] - fn test_dcommandlist_seed() { - let json = r#"{"default": "none", "add": ["/usr/bin/ls"], "del": ["/usr/bin/rm"]}"#; - let mut final_path = None; - let mut cmd_min = CmdMin::default(); - let deserializer = DCommandListDeserializer { - env_path: &["/usr/bin"], - cmd_path: &PathBuf::from("/usr/bin/ls"), - cmd_args: &[], - final_path: &mut final_path, - cmd_min: &mut cmd_min, - blocker: false, - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let result = result.unwrap(); - assert_eq!(final_path, Some(PathBuf::from("/usr/bin/ls"))); - assert!(result); - } - - #[test] - fn test_dcommand_seed() { - let json = r#""/usr/bin/ls""#; - let mut final_path = None; - let mut cmd_min = CmdMin::default(); - let deserializer = DCommandDeserializer { - env_path: &["/usr/bin"], - cmd_path: &PathBuf::from("/usr/bin/ls"), - cmd_args: &[], - final_path: &mut final_path, - cmd_min: &mut cmd_min, - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let result = result.unwrap(); - assert_eq!(final_path, Some(PathBuf::from("/usr/bin/ls"))); - assert!(result); - } - - #[test] - fn test_setuserdeserializerreturn() { - let json = - r#"{"default": "none", "fallback": "user1", "add": ["user2"], "del": ["user3"]}"#; - let cli = Cli::builder().build(); - let deserializer = SetUserDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (user, score, ok) = result.unwrap(); - assert!(ok); - let user1 = DUserType::from("user1"); - assert_eq!(score, Some(SetuidMin::from(&user1))); - assert_eq!(user, Some(user1)); - } - - #[test] - fn test_setuserdeserializerreturn_filter() { - let uid1 = get_non_root_uid(0).unwrap(); - let uid2 = get_non_root_uid(1).unwrap(); - let json = format!( - r#"{{"default": "none", "fallback": "root", "add": [{uid1}], "del": [{uid2}]}}"# - ); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) - .build(); - let deserializer = SetUserDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok()); - let (user, score, ok) = result.unwrap(); - assert!(ok); - let user1 = DUserType::from(uid1); - assert_eq!(score, Some(SetuidMin::from(&user1))); - assert_eq!(user, Some(user1)); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) - .build(); - let deserializer = SetUserDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok()); - let (user, score, ok) = result.unwrap(); - assert!(ok); - let user1 = DUserType::from("root"); - assert_eq!(score, Some(SetuidMin::from(&user1))); - assert_eq!(user, Some(user1)); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().user(uid2).unwrap().build()) - .build(); - let deserializer = SetUserDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok()); - let (user, score, ok) = result.unwrap(); - assert!(!ok); - assert_eq!(score, None); - assert_eq!(user, None); - let json = "\"root\""; - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) - .build(); - let deserializer = SetUserDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let (user, score, ok) = result.unwrap(); - assert!(ok); - let user1 = DUserType::from("root"); - assert_eq!(score, Some(SetuidMin::from(&user1))); - assert_eq!(user, Some(user1)); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) - .build(); - let deserializer = SetUserDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (user, score, ok) = result.unwrap(); - assert!(!ok); - assert_eq!(score, None); - assert_eq!(user, None); - let json = "0"; - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) - .build(); - let deserializer = SetUserDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (user, score, ok) = result.unwrap(); - assert!(!ok); - assert_eq!(score, None); - assert_eq!(user, None); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) - .build(); - let deserializer = SetUserDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (user, score, ok) = result.unwrap(); - assert!(ok); - let user1 = DUserType::from(0); - assert_eq!(score, Some(SetuidMin::from(&user1))); - assert_eq!(user, Some(user1)); - } - - #[test] - fn test_no_fallback() { - let json = r#"{"default": "all"}"#; - let cli = Cli::builder().build(); - let deserializer = SetUserDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (user, score, ok) = result.unwrap(); - assert!(ok); - assert_eq!(score, None); - assert_eq!(user, None); - } - - #[test] - fn test_setgroupsdeserializerreturn() { - let json = r#"{"default": "none", "fallback": [1, 2], "add": [[3, 4]], "del": [[5, 6]]}"#; - let cli = Cli::builder().build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (groups, score, ok) = result.unwrap(); - assert!(ok); - let groups1 = DGroups::from(vec![1.into(), 2.into()]); - assert_eq!(score, Some((&groups1).into())); - assert_eq!(groups, Some(groups1)); - } - - #[test] - fn test_setgroupsdeserializerreturn_filter() { - let gid1 = get_non_root_gid(0).unwrap(); - let gid2 = get_non_root_gid(1).unwrap(); - let json = format!( - r#"{{"default": "none", "fallback": ["root"], "add": [[{gid1}]], "del": [[{gid2}]]}}"# - ); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok()); - let (groups, score, ok) = result.unwrap(); - assert!(ok); - let groups1 = DGroups::Single("root".into()); - assert_eq!(score, Some((&groups1).into())); - assert_eq!(groups, Some(groups1)); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok()); - let (groups, score, ok) = result.unwrap(); - assert!(ok); - let groups1 = DGroups::Single(gid1.into()); - assert_eq!(score, Some((&groups1).into())); - assert_eq!(groups, Some(groups1)); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().group(gid2).unwrap().build()) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok()); - let (groups, score, ok) = result.unwrap(); - assert!(!ok); - assert_eq!(score, None); - assert_eq!(groups, None); - let json = "\"root\""; - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let (groups, score, ok) = result.unwrap(); - assert!(ok); - let groups1 = DGroups::Single("root".into()); - assert_eq!(score, Some((&groups1).into())); - assert_eq!(groups, Some(groups1)); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (groups, score, ok) = result.unwrap(); - assert!(!ok); - assert_eq!(score, None); - assert_eq!(groups, None); - let json = "0"; - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (groups, score, ok) = result.unwrap(); - assert!(!ok); - assert_eq!(score, None); - assert_eq!(groups, None); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (groups, score, ok) = result.unwrap(); - assert!(ok); - let groups1 = DGroups::Single(0.into()); - assert_eq!(score, Some((&groups1).into())); - assert_eq!(groups, Some(groups1)); - let json = "[[\"root\", 1]]"; - let cli = Cli::builder() - .opt_filter( - FilterMatcher::builder() - .group(vec!["root".into(), Into::::into(1)]) - .unwrap() - .build(), - ) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let (groups, score, ok) = result.unwrap(); - assert!(ok); - let groups1 = DGroups::from(vec!["root".into(), 1.into()]); - assert_eq!(score, Some((&groups1).into())); - assert_eq!(groups, Some(groups1)); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().build()) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let (groups, score, ok) = result.unwrap(); - assert!(ok); - let groups1 = DGroups::from(vec!["root".into(), 1.into()]); - assert_eq!(score, Some((&groups1).into())); - assert_eq!(groups, Some(groups1)); - let cli = Cli::builder() - .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) - .build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (groups, score, ok) = result.unwrap(); - assert!(!ok); - assert_eq!(score, None); - assert_eq!(groups, None); - } - - #[test] - fn test_no_fallback_groups() { - let json = r#"{"default": "all"}"#; - let cli = Cli::builder().build(); - let deserializer = SetGroupsDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok()); - let (groups, score, ok) = result.unwrap(); - assert!(ok); - assert_eq!(score, None); - assert_eq!(groups, None); - } - - #[test] - fn test_cred_deserializer() { - let json = r#"{"setuid":"root", "setgid":"root", "caps": ["CAP_SYS_ADMIN"]}"#; - let cli = Cli::builder().build(); - let deserializer = CredFinderDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let result = result.unwrap(); - assert!(result.ok); - assert_eq!(result.cred.setuid, Some("root".into())); - assert_eq!( - result.cred.setgroups, - Some(DGroups::from(vec!["root".into()])) - ); - assert_eq!( - result.cred.caps, - Some(CapSet::from_iter(vec![Cap::SYS_ADMIN])) - ); - assert_eq!( - result.score.setuser_min.uid, - Some(SetuidMin::from(&"root".into())) - ); - assert_eq!( - result.score.setuser_min.gid, - Some(SetgidMin::from(&Into::>::into("root"))) - ); - assert_eq!(result.score.caps_min, CapsMin::CapsAdmin(1)); - - let uid = get_non_root_uid(0).unwrap(); - let gid = get_non_root_gid(0).unwrap(); - let json = format!(r#"{{"setuid":{uid}, "setgid":[[{gid}]]}}"#); - let cli = Cli::builder().build(); - let deserializer = CredFinderDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let result = result.unwrap(); - assert!(result.ok); - assert_eq!(result.cred.setuid, Some(uid.into())); - assert_eq!(result.cred.setgroups, Some(DGroups::from(vec![gid.into()]))); - assert_eq!(result.cred.caps, None); - assert_eq!( - result.score.setuser_min.uid, - Some(SetuidMin::from(&uid.into())) - ); - assert_eq!( - result.score.setuser_min.gid, - Some(SetgidMin::from(&Into::>::into(uid))) - ); - assert_eq!(result.score.caps_min, CapsMin::Undefined); - - let uid = get_non_root_uid(0).unwrap(); - let gid = get_non_root_gid(0).unwrap(); - let json = format!(r#"{{"setuid":"{uid}", "setgid":["{gid}"]}}"#); - let cli = Cli::builder().build(); - let deserializer = CredFinderDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let result = result.unwrap(); - assert!(result.ok); - assert_eq!(result.cred.setuid, Some(uid.into())); - assert_eq!(result.cred.setgroups, Some(DGroups::from(vec![gid.into()]))); - assert_eq!(result.cred.caps, None); - assert_eq!( - result.score.setuser_min.uid, - Some(SetuidMin::from(&uid.into())) - ); - assert_eq!( - result.score.setuser_min.gid, - Some(SetgidMin::from(&Into::>::into(uid))) - ); - assert_eq!(result.score.caps_min, CapsMin::Undefined); - } - - #[test] - fn test_cred_deserializer_invalid() { - let json = r#"{"setuid":-1, "setgid":"invalid", "caps": ["CAP_SYS_ADMIN"]}"#; - let cli = Cli::builder().build(); - let deserializer = CredFinderDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - let json = r#"{"setuid":"invalid", "setgid":-1, "caps": ["CAP_SYS_ADMIN"]}"#; - let cli = Cli::builder().build(); - let deserializer = CredFinderDeserializerReturn { cli: &cli }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - } - - #[test] - fn test_task_deserializer() { - let json = r#"{"name": "test", "cred": {"setuid":"0", "setgid":["0", 0], "caps": []}, "commands": ["ls"]}}"#; - let cli = Cli::builder().build(); - let deserializer = TaskFinderDeserializer { - cli: &cli, - i: 0, - env_path: &[], - spath: &mut DPathOptions::default(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let task = result.unwrap().unwrap(); - assert_eq!(task.id, IdTask::Name("test".into())); - assert_eq!(task.score.setuser_min.uid, Some(SetuidMin::from(&0.into()))); - assert_eq!(task.score.setuser_min.gid, Some(SetgidMin::from(&vec![0]))); - assert_eq!(task.score.caps_min, CapsMin::NoCaps); - let commands = task.commands.unwrap(); - assert_eq!(commands.add.len(), 1); - assert_eq!(commands.add[0], DCommand::Simple("ls".into())); - } - - #[test] - fn test_task_list_deserializer() { - let json = r#"[{"name": "test", "cred": {"setuid":"0", "setgid":["0", 0], "caps": []}, "commands": ["ls"]}]"#; - let cli = Cli::builder().build(); - let deserializer = TaskListFinderDeserializer { - cli: &cli, - env_path: &[], - spath: &mut DPathOptions::default(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let task = &result.unwrap()[0]; - assert_eq!(task.id, IdTask::Name("test".into())); - assert_eq!(task.score.setuser_min.uid, Some(SetuidMin::from(&0.into()))); - assert_eq!(task.score.setuser_min.gid, Some(SetgidMin::from(&vec![0]))); - assert_eq!(task.score.caps_min, CapsMin::NoCaps); - let commands = task.commands.as_ref().unwrap(); - assert_eq!(commands.add.len(), 1); - assert_eq!(commands.add[0], DCommand::Simple("ls".into())); - } - - #[test] - fn test_actors_finder_deserializer() { - let json = format!(r#"[{{"type": "user", "id": {}}}]"#, getuid().as_raw()); - let deserializer = ActorsFinderDeserializer { - cred: &Cred::builder().build(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let user_min = result.unwrap(); - assert_eq!(user_min, ActorMatchMin::UserMatch); - } - - #[test] - fn test_role_finder_deserializer() { - let json = format!( - r#"{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}"#, - getuid().as_raw() - ); - let cli = Cli::builder().cmd_path("ls").build(); - let deserializer = RoleFinderDeserializer { - cli: &cli, - env_path: &["/usr/bin"], - cred: &Cred::builder().build(), - spath: &mut DPathOptions::default(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let role = result.unwrap().unwrap(); - assert_eq!(role.role, "r_test"); - assert_eq!(role.tasks.len(), 1); - assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); - } - - #[test] - fn test_role_list_finder_deserializer() { - let json = format!( - r#"[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"#, - getuid().as_raw() - ); - let cli = Cli::builder().cmd_path("ls").build(); - let deserializer = RoleListFinderDeserializer { - cli: &cli, - env_path: &["/usr/bin"], - cred: &Cred::builder().build(), - spath: &mut DPathOptions::default(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let role = &result.unwrap()[0]; - assert_eq!(role.role, "r_test"); - assert_eq!(role.tasks.len(), 1); - assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); - let json = format!( - r#"[{{"name":"r_test","actors":[{{"type": "group", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"#, - getgid().as_raw() - ); - let cli = Cli::builder().cmd_path("ls").build(); - let deserializer = RoleListFinderDeserializer { - cli: &cli, - env_path: &["/usr/bin"], - cred: &Cred::builder().build(), - spath: &mut DPathOptions::default(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let role = &result.unwrap()[0]; - assert_eq!(role.role, "r_test"); - assert_eq!(role.tasks.len(), 1); - assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); - let json = r#"[{"name":"r_test","actors":[{"type": "user", "id": "874510"}], "tasks": [{"name": "test", "cred": {"setuid":"0", "setgid":["0", 0], "caps": []}, "commands": ["/usr/bin/ls"]}]}]"#.to_string(); - let cli = Cli::builder().cmd_path("ls").build(); - let deserializer = RoleListFinderDeserializer { - cli: &cli, - env_path: &["/usr/bin"], - cred: &Cred::builder().build(), - spath: &mut DPathOptions::default(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let result = result.unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0].user_min, ActorMatchMin::NoMatch); - } - - #[test] - fn test_config_finder_deserializer() { - let json = format!( - r#"{{"roles":[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]}}"#, - getuid().as_raw() - ); - let cli = Cli::builder().cmd_path("ls").build(); - let deserializer = ConfigFinderDeserializer { - cli: &cli, - env_path: &["/usr/bin"], - cred: &Cred::builder().build(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let config = result.unwrap(); - assert_eq!(config.roles.len(), 1); - assert_eq!(config.roles[0].role, "r_test"); - } - - #[test] - fn test_config_finder_implementation() { - let json = format!( - r#"{{"roles":[{{"name":"r_test","actors":[{{"type":"user","id":{}}}],"tasks":[{{"name":"test","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/ls"]}},{{"name":"test2","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/ls","/usr/bin/cat"]}}]}},{{"name":"r_test2","actors":[{{"type":"group","names":[{}, {}]}}],"tasks":[{{"name":"test3","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/cat","/usr/bin/ls"]}}]}}]}}"#, - getuid().as_raw(), - getgid().as_raw(), - getgid().as_raw() - ); - let cli = Cli::builder().cmd_path("ls").build(); - let deserializer = ConfigFinderDeserializer { - cli: &cli, - env_path: &["/usr/bin"], - cred: &Cred::builder().build(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let config = result.unwrap(); - let mut roles = config.roles(); - let role_a = roles.next().unwrap(); - assert_eq!(role_a.role().role, "r_test"); - let mut tasks = role_a.tasks(); - let task_a = tasks.next().unwrap(); - assert_eq!(task_a.task().id, IdTask::Name("test".into())); - let commands = task_a.commands().unwrap(); - assert_eq!(commands.add().count(), 1); - assert_eq!( - *commands.add().next().unwrap().command, - DCommand::Simple("/usr/bin/ls".into()) - ); - let task_b = tasks.next().unwrap(); - assert_eq!(task_b.task().id, IdTask::Name("test2".into())); - let commands = task_b.commands().unwrap(); - assert_eq!(commands.add().count(), 2); - assert_eq!( - *commands.add().next().unwrap().command, - DCommand::Simple("/usr/bin/ls".into()) - ); - assert_eq!( - *commands.add().nth(1).unwrap().command, - DCommand::Simple("/usr/bin/cat".into()) - ); - assert!(tasks.next().is_none()); - let role_b = roles.next().unwrap(); - assert_eq!(role_b.role().role, "r_test2"); - let mut tasks = role_b.tasks(); - let task_a = tasks.next().unwrap(); - assert_eq!(task_a.task().id, IdTask::Name("test3".into())); - let commands = task_a.commands().unwrap(); - assert_eq!(commands.add().count(), 2); - assert_eq!( - *commands.add().next().unwrap().command, - DCommand::Simple("/usr/bin/cat".into()) - ); - assert_eq!( - *commands.add().nth(1).unwrap().command, - DCommand::Simple("/usr/bin/ls".into()) - ); - assert_eq!(commands.del().count(), 0); - assert!(tasks.next().is_none()); - assert!(roles.next().is_none()); - assert!(config.options.is_none()); - assert!(config.roles[0].options.is_none()); - assert!(config.roles[0].tasks[0].options.is_none()); - assert!(config.roles[0].tasks[1].options.is_none()); - assert!(config.roles[1].options.is_none()); - assert!(config.roles[1].tasks[0].options.is_none()); - assert!(config.role("r_test").is_some()); - assert!(config.role("r_test2").is_some()); - assert!(config.role("r_test3").is_none()); - assert_eq!(*config.role("r_test").unwrap().config(), config); - assert_eq!(*config.role("r_test2").unwrap().config(), config); - assert_eq!( - *config - .role("r_test") - .unwrap() - .tasks() - .next() - .unwrap() - .role(), - config.role("r_test").unwrap() - ); - assert_eq!( - *config - .role("r_test2") - .unwrap() - .tasks() - .next() - .unwrap() - .role(), - config.role("r_test2").unwrap() - ); - assert_eq!( - config - .role("r_test") - .unwrap() - .tasks() - .next() - .unwrap() - .score(CmdMin::MATCH, SecurityMin::empty()), - Score::builder() - .user_min(ActorMatchMin::UserMatch) - .setuser_min(SetUserMin { - uid: Some(SetuidMin::from(0)), - gid: Some(SetgidMin::from(SGroups::from(vec![0]))) - }) - .caps_min(CapsMin::NoCaps) - .security_min(SecurityMin::empty()) - .cmd_min(CmdMin::MATCH) - .build() - ); - } - - #[test] - fn test_config_with_options() { - let json = format!( - r#"{{ - "options": {{ - "timeout": {{ - "type": "ppid", - "duration": "00:05:00" - }}, - "path": {{ - "default": "delete", - "add": [ - "/usr/bin" - ] - }}, - "env": {{ - "default": "delete", - "override_behavior": false, - "keep": [ - "keep1" - ], - "check": [ - "check1" - ], - "delete": [ - "del1" - ], - "set": {{ - "set1": "value1", - "set2": "value2" - }} - }}, - "root": "user", - "bounding": "strict" - }}, - "roles": [ - {{ - "options": {{ - "timeout": {{ - "type": "ppid", - "duration": "00:06:00" - }}, - "path": {{ - "default": "delete", - "add": [ - "/usr/bin" - ] - }}, - "env": {{ - "default": "delete", - "override_behavior": false, - "keep": [ - "keep2" - ], - "check": [ - "check2" - ], - "delete": [ - "del2" - ], - "set": {{ - "set1": "value2", - "set3": "value3" - }} - }}, - "root": "user", - "bounding": "strict" - }}, - "name": "role1", - "actors": [ - {{ - "type": "user", - "id": {} - }} - ], - "tasks": [ - {{ - "options": {{ - "timeout": {{ - "type": "ppid", - "duration": "00:07:00" - }}, - "path": {{ - "default": "delete", - "add": [ - "/usr/bin" - ] - }}, - "env": {{ - "default": "delete", - "override_behavior": false, - "keep": [ - "keep3" - ], - "check": [ - "check3" - ], - "delete": [ - "del3" - ], - "set": {{ - "set1": "value3", - "set4": "value4" - }} - }}, - "root": "user", - "bounding": "strict" - }}, - "name": "task1", - "cred": {{ - "setuid": 0, - "setgid": 0, - "caps": [ - "CAP_SYS_ADMIN", - "CAP_SYS_RESOURCE" - ] - }} - }} - ] - }} - ] -}}"#, - getuid().as_raw() - ); - let cli = Cli::builder().cmd_path("ls").build(); - let deserializer = ConfigFinderDeserializer { - cli: &cli, - env_path: &["/usr/bin"], - cred: &Cred::builder().build(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let config = result.unwrap(); - assert_eq!(config.roles.len(), 1); - assert_eq!(config.roles[0].role, "role1"); - assert_eq!(config.roles[0].tasks.len(), 1); - assert_eq!(config.roles[0].tasks[0].id, IdTask::Name("task1".into())); - assert!(config.options.is_some()); - assert!(config.roles[0].options.is_some()); - assert!(config.roles[0].tasks[0].options.is_some()); - } - - #[test] - fn test_config_optimized_with_options() { - let json = format!( - r#"{{ - "options": {{ - "timeout": {{ - "type": "ppid", - "duration": "00:05:00" - }}, - "path": {{ - "default": "delete", - "add": [ - "/usr/bin" - ] - }}, - "env": {{ - "default": "delete", - "override_behavior": false, - "keep": [ - "keep1" - ], - "check": [ - "check1" - ], - "delete": [ - "del1" - ], - "set": {{ - "set1": "value1", - "set2": "value2" - }} - }}, - "root": "user", - "bounding": "strict" - }}, - "roles": [ - {{ - "options": {{ - "timeout": {{ - "type": "ppid", - "duration": "00:06:00" - }}, - "path": {{ - "default": "delete", - "add": [ - "/usr/bin" - ] - }}, - "env": {{ - "default": "delete", - "override_behavior": false, - "keep": [ - "keep2" - ], - "check": [ - "check2" - ], - "delete": [ - "del2" - ], - "set": {{ - "set1": "value2", - "set3": "value3" - }} - }}, - "root": "user", - "bounding": "strict" - }}, - "name": "role1", - "actors": [ - {{ - "type": "group", - "id": {} - }} - ], - "tasks": [ - {{ - "options": {{ - "timeout": {{ - "type": "ppid", - "duration": "00:07:00" - }}, - "path": {{ - "default": "delete", - "add": [ - "/usr/bin" - ] - }}, - "env": {{ - "default": "delete", - "override_behavior": false, - "keep": [ - "keep3" - ], - "check": [ - "check3" - ], - "delete": [ - "del3" - ], - "set": {{ - "set1": "value3", - "set4": "value4" - }} - }}, - "root": "user", - "bounding": "strict" - }}, - "name": "task1", - "cred": {{ - "setuid": 0, - "setgid": 0, - "caps": [ - "CAP_SYS_ADMIN", - "CAP_SYS_RESOURCE" - ] - }}, - "commands": ["/usr/bin/ls"] - }} - ] - }} - ] -}}"#, - getgid().as_raw() - ); - //convert json to cbor4ii - let cbor = convert_json_to_cbor(&json); - let cli = Cli::builder().cmd_path("ls").build(); - let deserializer = ConfigFinderDeserializer { - cli: &cli, - env_path: &["/usr/bin"], - cred: &Cred::builder().build(), - }; - let result: Result, _> = deserializer.deserialize( - &mut cbor4ii::serde::Deserializer::new(SliceReader::new(cbor.as_slice())), - ); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let config = result.unwrap(); - assert_eq!(config.roles.len(), 1); - assert_eq!(config.roles[0].role, "role1"); - assert_eq!(config.roles[0].tasks.len(), 1); - assert_eq!(config.roles[0].tasks[0].id, IdTask::Name("task1".into())); - assert!(config.options.is_some()); - assert!(config.roles[0].options.is_some()); - assert!(config.roles[0].tasks[0].options.is_some()); - assert_eq!(config.roles[0].user_min, ActorMatchMin::GroupMatch(1)); - assert_eq!(config.roles[0].tasks[0].score.cmd_min, CmdMin::MATCH); - assert_eq!( - config.roles[0].tasks[0].score.setuser_min.uid, - Some(SetuidMin::from(&0.into())) - ); - assert_eq!( - config.roles[0].tasks[0].score.setuser_min.gid, - Some(SetgidMin::from(&vec![0])) - ); - assert_eq!( - config.roles[0].tasks[0].score.caps_min, - CapsMin::CapsAdmin(2) - ); - assert!(config.roles[0].tasks[0].commands.is_none()); - assert_eq!( - config.roles[0].tasks[0].final_path, - Some(PathBuf::from("/usr/bin/ls")) - ); - } - - #[test] - fn test_optimized_config() { - let uid = getuid().as_raw(); - let json = format!( - r#"{{"roles":[{{"name":"r_test","actors":[{{"type": "user", "id": {uid}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0"], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]}}"# - ); - //convert json to cbor4ii - let cbor = convert_json_to_cbor(&json); - let cli = Cli::builder().cmd_path("ls").build(); - let deserializer = ConfigFinderDeserializer { - cli: &cli, - env_path: &["/usr/bin"], - cred: &Cred::builder().build(), - }; - let result: Result, _> = deserializer.deserialize( - &mut cbor4ii::serde::Deserializer::new(SliceReader::new(cbor.as_slice())), - ); - assert!(result.is_ok(), "Failed to deserialize: {result:?}"); - let config = result.unwrap(); - assert_eq!(config.roles[0].user_min, ActorMatchMin::UserMatch); - assert_eq!(config.roles[0].tasks[0].score.cmd_min, CmdMin::MATCH); - assert_eq!( - config.roles[0].tasks[0].score.setuser_min.uid, - Some(SetuidMin::from(&0.into())) - ); - assert_eq!( - config.roles[0].tasks[0].score.setuser_min.gid, - Some(SetgidMin::from(&vec![0])) - ); - assert_eq!(config.roles[0].tasks[0].score.caps_min, CapsMin::NoCaps); - assert!(config.roles[0].tasks[0].commands.is_none()); - assert_eq!( - config.roles[0].tasks[0].final_path, - Some(PathBuf::from("/usr/bin/ls")) - ); - } - - #[test] - fn test_expecting_error() { - let seq = "[1, 2, 3]"; - let map = "{\"1\": 2, \"3\": 4}"; - let int = "1"; - let float = "1.0"; - let cli = Cli::builder().build(); - let config_finder = ConfigFinderDeserializer { - cli: &cli, - env_path: &[], - cred: &Cred::builder().build(), - }; - let result = config_finder.deserialize(&mut serde_json::Deserializer::from_str(seq)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - - let role_list = RoleListFinderDeserializer { - cli: &cli, - env_path: &[], - cred: &Cred::builder().build(), - spath: &mut DPathOptions::default(), - }; - let result = role_list.deserialize(&mut serde_json::Deserializer::from_str(map)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - let task_list = TaskListFinderDeserializer { - cli: &cli, - env_path: &[], - spath: &mut DPathOptions::default(), - }; - let result = task_list.deserialize(&mut serde_json::Deserializer::from_str(map)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - let task = TaskFinderDeserializer { - cli: &cli, - i: 0, - env_path: &[], - spath: &mut DPathOptions::default(), - }; - let result = task.deserialize(&mut serde_json::Deserializer::from_str(seq)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - assert!(serde_json::from_str::(int).is_err()); - let mut var_name = None; - let mut cmd_min = CmdMin::MATCH; - let dcommand = DCommandDeserializer { - env_path: &[], - cmd_path: &cli.cmd_path, - cmd_args: &cli.cmd_args, - final_path: &mut var_name, - cmd_min: &mut cmd_min, - }; - let result = dcommand.deserialize(&mut serde_json::Deserializer::from_str(seq)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - let cred = CredFinderDeserializerReturn { cli: &cli }; - let result = cred.deserialize(&mut serde_json::Deserializer::from_str(seq)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - let setuser = SetUserDeserializerReturn { cli: &cli }; - let result = setuser.deserialize(&mut serde_json::Deserializer::from_str(float)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - let setgroups = SetGroupsDeserializerReturn { cli: &cli }; - let result = setgroups.deserialize(&mut serde_json::Deserializer::from_str(float)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - let actors = ActorsFinderDeserializer { - cred: &Cred::builder().build(), - }; - let result = actors.deserialize(&mut serde_json::Deserializer::from_str(int)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - let role = RoleFinderDeserializer { - cli: &cli, - env_path: &[], - cred: &Cred::builder().build(), - spath: &mut DPathOptions::default(), - }; - let result = role.deserialize(&mut serde_json::Deserializer::from_str(int)); - assert!(result.is_err(), "Expected error, got: {result:?}"); - } - - // this test is to check if the deserializer can handle unknown types... It might evolve in the future - #[test] - fn test_unknown_type() { - let json = r#"{"unknown": "unknown"}"#; - let cli = Cli::builder().build(); - let deserializer = ConfigFinderDeserializer { - cli: &cli, - env_path: &[], - cred: &Cred::builder().build(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!( - result.is_ok(), - "Expected config with nothing in it, got: {result:?}" - ); - - let deserializer = RoleFinderDeserializer { - cli: &cli, - env_path: &[], - cred: &Cred::builder().build(), - spath: &mut DPathOptions::default(), - }; - let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!( - result.is_ok(), - "Expected role with nothing in it, got: {result:?}" - ); - - let deserializer = TaskFinderDeserializer { - cli: &cli, - i: 0, - env_path: &[], - spath: &mut DPathOptions::default(), - }; - let result: Result>, serde_json::Error> = - deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); - assert!( - result.is_ok(), - "Expected task with nothing in it, got: {result:?}" - ); - } -} diff --git a/src/sr/finder/de/commands.rs b/src/sr/finder/de/commands.rs new file mode 100644 index 00000000..3068a5d5 --- /dev/null +++ b/src/sr/finder/de/commands.rs @@ -0,0 +1,402 @@ +use std::{borrow::Cow, path::PathBuf, str::FromStr}; + +use log::debug; +use rar_common::database::{score::CmdMin, structs::SetBehavior}; +use serde::{ + Deserialize, + de::{DeserializeSeed, IgnoredAny}, +}; +use serde_json::Value; + +use crate::finder::{ + api::{Api, ApiEvent}, + cmd, + de::{DCommand, DCommandList}, +}; + +impl<'de: 'a, 'a> Deserialize<'de> for DCommandList<'a> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d", alias = "default_behavior")] + Default, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub", alias = "del")] + Del, + } + #[derive(Default)] + struct DCommandListVisitor<'a> { + _phantom: std::marker::PhantomData<&'a ()>, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandListVisitor<'a> { + type Value = DCommandList<'a>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("CommandList structure") + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut default_behavior = None; + let mut add: Cow<'_, [DCommand<'_>]> = Cow::Borrowed(&[]); + let mut del: Cow<'_, [DCommand<'_>]> = Cow::Borrowed(&[]); + while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("DCommandListVisitor: default"); + default_behavior = Some(map.next_value()?); + } + Field::Add => { + debug!("DCommandListVisitor: add"); + add = map.next_value()?; + } + Field::Del => { + debug!("DCommandListVisitor: del"); + del = map.next_value()?; + } + } + } + Ok(DCommandList { + default_behavior, + add, + del, + }) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut add = Vec::new(); + while let Some(command) = seq.next_element()? { + add.push(command); + } + Ok(DCommandList { + default_behavior: None, + add: Cow::Owned(add), + del: Cow::Borrowed(&[]), + }) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + self.visit_str(&v) + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let set = SetBehavior::from_str(v).map_err(serde::de::Error::custom)?; + Ok(DCommandList { + default_behavior: Some(set), + add: Cow::Borrowed(&[]), + del: Cow::Borrowed(&[]), + }) + } + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + self.visit_str(v) + } + } + deserializer.deserialize_any(DCommandListVisitor::default()) + } +} + +/// This struct evaluates commands directly from deserialization +pub struct DCommandListDeserializer<'a> { + pub env_path: &'a [&'a str], + pub cmd_path: &'a PathBuf, + pub cmd_args: &'a [String], + pub final_path: &'a mut Option, + pub cmd_min: &'a mut CmdMin, + pub blocker: bool, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for DCommandListDeserializer<'a> { + type Value = bool; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(self) + } +} + +impl DCommandListDeserializer<'_> { + const fn generate_dcommand_deserializer(&mut self) -> DCommandDeserializer<'_> { + DCommandDeserializer { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + } + } +} + +impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandListDeserializer<'a> { + type Value = bool; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("CommandList Deserializer structure") + } + + fn visit_seq(mut self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut result = false; + while let Some(bool) = seq.next_element_seed(self.generate_dcommand_deserializer())? { + if bool && self.blocker { + return Ok(true); + } + result |= bool; + } + Ok(result) + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d", alias = "default_behavior")] + Default, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub")] + Del, + } + let mut result = false; + let mut default = SetBehavior::None; + while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("DCommandListVisitor: default"); + default = map.next_value()?; + } + Field::Del => { + let deserializer = DCommandListDeserializer { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + blocker: true, + }; + let res = map.next_value_seed(deserializer)?; + if res { + while map.next_entry::()?.is_some() {} + return Ok(false); + } + } + Field::Add => { + if default.is_all() { + let _ = map.next_value::(); + } else { + let deserializer = DCommandListDeserializer { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + blocker: false, + }; + result |= map.next_value_seed(deserializer)?; + } + } + } + } + Ok(result) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + self.visit_str(&v) + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let set = SetBehavior::from_str(v).map_err(serde::de::Error::custom)?; + Ok(set.is_all()) + } + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + self.visit_str(v) + } +} + +pub struct DCommandDeserializer<'a> { + pub env_path: &'a [&'a str], + pub cmd_path: &'a PathBuf, + pub cmd_args: &'a [String], + pub final_path: &'a mut Option, + pub cmd_min: &'a mut CmdMin, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for DCommandDeserializer<'a> { + type Value = bool; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct DCommandVisitor<'a> { + env_path: &'a [&'a str], + cmd_path: &'a PathBuf, + cmd_args: &'a [String], + final_path: &'a mut Option, + cmd_min: &'a mut CmdMin, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for DCommandVisitor<'a> { + type Value = bool; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Command structure") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let mut final_path = None; + let mut result = false; + debug!("DCommandVisitor: command {v}"); + let cmd_min = cmd::evaluate_command_match( + self.env_path, + self.cmd_path, + self.cmd_args, + v, + *self.cmd_min, + &mut final_path, + ); + debug!("DCommandVisitor: command result {cmd_min:?}"); + if cmd_min.better(*self.cmd_min) { + debug!("DCommandVisitor: better command found"); + result = true; + *self.final_path = final_path; + *self.cmd_min = cmd_min; + } + Ok(result) + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut map_value = Vec::new(); + while let Some((key, value)) = map.next_entry::<&str, Value>()? { + map_value.push((key, value)); + } + Api::notify(ApiEvent::ProcessComplexCommand( + &Value::Object( + map_value + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), + ), + self.env_path, + self.cmd_path, + self.cmd_args, + self.cmd_min, + self.final_path, + )) + .map(|()| true) + .map_err(|_| serde::de::Error::custom("Failed to notify process complex command")) + } + } + deserializer.deserialize_any(DCommandVisitor { + env_path: self.env_path, + cmd_path: self.cmd_path, + cmd_args: self.cmd_args, + final_path: self.final_path, + cmd_min: self.cmd_min, + }) + } +} + +#[cfg(test)] +mod test { + use std::path::PathBuf; + + use rar_common::database::score::CmdMin; + use serde::de::DeserializeSeed; + + use crate::{ + Cli, + finder::de::commands::{DCommandDeserializer, DCommandListDeserializer}, + }; + + #[test] + fn test_dcommandlist_seed() { + let json = r#"{"default": "none", "add": ["/usr/bin/ls"], "del": ["/usr/bin/rm"]}"#; + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandListDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from("/usr/bin/ls"), + cmd_args: &[], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + blocker: false, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(final_path, Some(PathBuf::from("/usr/bin/ls"))); + assert!(result); + } + + #[test] + fn test_dcommand_seed() { + let json = r#""/usr/bin/ls""#; + let mut final_path = None; + let mut cmd_min = CmdMin::default(); + let deserializer = DCommandDeserializer { + env_path: &["/usr/bin"], + cmd_path: &PathBuf::from("/usr/bin/ls"), + cmd_args: &[], + final_path: &mut final_path, + cmd_min: &mut cmd_min, + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let result = result.unwrap(); + assert_eq!(final_path, Some(PathBuf::from("/usr/bin/ls"))); + assert!(result); + } + + #[test] + fn test_expecting_errors() { + let seq = "[1, 2, 3]"; + let mut var_name = None; + let cli = Cli::builder().build(); + let mut cmd_min = CmdMin::MATCH; + let dcommand = DCommandDeserializer { + env_path: &[], + cmd_path: &cli.cmd_path, + cmd_args: &cli.cmd_args, + final_path: &mut var_name, + cmd_min: &mut cmd_min, + }; + let result = dcommand.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + } +} diff --git a/src/sr/finder/de/cred.rs b/src/sr/finder/de/cred.rs new file mode 100644 index 00000000..2925a2e8 --- /dev/null +++ b/src/sr/finder/de/cred.rs @@ -0,0 +1,942 @@ +use std::{borrow::Cow, collections::HashMap}; + +use bon::Builder; +use capctl::CapSet; +use log::debug; +use nix::unistd::{Group, User}; +use rar_common::{ + database::{ + actor::{DGroupType, DGroups, DUserType}, + score::{CapsMin, SetgidMin, SetuidMin, TaskScore}, + structs::{SCapabilities, SetBehavior}, + }, + util::capabilities_are_exploitable, +}; +use serde::{ + Deserialize, + de::{DeserializeSeed, IgnoredAny}, +}; +use serde_json::Value; + +use crate::Cli; + +pub(super) struct CredFinderDeserializerReturn<'a> { + pub(super) cli: &'a Cli, +} + +#[derive(Debug, PartialEq, Eq, Default, Builder)] +pub struct CredData<'a> { + pub setuid: Option>, + pub setgroups: Option>, + pub caps: Option, + #[builder(default)] + pub extra_values: HashMap, Value>, +} + +#[derive(Debug, PartialEq, Eq, Default, Clone, Builder)] +pub struct CredOwnedData { + pub setuid: Option, + pub setgroups: Option>, + pub caps: Option, + #[builder(default)] + pub extra_values: HashMap, +} + +#[derive(Debug)] +pub(super) struct CredResult<'a> { + pub(super) cred: CredData<'a>, + pub(super) score: TaskScore, + pub(super) ok: bool, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for CredFinderDeserializerReturn<'a> { + type Value = CredResult<'a>; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field<'a> { + #[serde(alias = "u")] + Setuid, + #[serde(alias = "g", alias = "setgroups")] + Setgid, + #[serde(alias = "c", alias = "capabilities")] + Caps, + #[serde(untagged, borrow)] + Other(Cow<'a, str>), + } + + struct CredFinderVisitor<'a> { + cli: &'a Cli, + } + + fn get_caps_min(caps: CapSet) -> CapsMin { + if caps.is_empty() { + CapsMin::NoCaps + } else if caps == !CapSet::empty() { + CapsMin::CapsAll + } else if capabilities_are_exploitable(caps) { + CapsMin::CapsAdmin(caps.size()) + } else { + CapsMin::CapsNoAdmin(caps.size()) + } + } + + impl<'de: 'a, 'a> serde::de::Visitor<'de> for CredFinderVisitor<'a> { + type Value = CredResult<'a>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("Cred structure") + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut setuid = None; + let mut setgroups = None; + let mut caps = None; + let mut score = TaskScore::default(); + let mut ok = true; + let mut extra_values = HashMap::new(); + while let Some(key) = map.next_key()? { + match key { + Field::Setuid => { + debug!("CredFinderVisitor: setuid"); + let (user, setuser_min, user_ok) = + map.next_value_seed(SetUserDeserializerReturn { cli: self.cli })?; + setuid = user; + score.setuser_min.uid = setuser_min; + if !user_ok { + ok = false; + } + } + Field::Setgid => { + debug!("CredFinderVisitor: setgid"); + let (groups, setuser_min, groups_ok) = + map.next_value_seed(SetGroupsDeserializerReturn { cli: self.cli })?; + setgroups = groups; + score.setuser_min.gid = setuser_min; + if !groups_ok { + ok = false; + } + } + Field::Caps => { + debug!("CredFinderVisitor: capabilities"); + let scaps: SCapabilities = map.next_value()?; + let capset = scaps.to_capset(); + score.caps_min = get_caps_min(capset); + caps = Some(capset); + } + Field::Other(n) => { + debug!("CredFinderVisitor: unknown {n}"); + let v: Value = map.next_value()?; + extra_values.insert(n, v); + } + } + } + debug!("CredFinderVisitor: end"); + Ok(CredResult { + cred: CredData { + setuid, + setgroups, + caps, + extra_values, + }, + score, + ok, + }) + } + } + const FIELDS: &[&str] = &["setuid", "setgroups", "capabilities", "0", "1", "2"]; + deserializer.deserialize_struct("Cred", FIELDS, CredFinderVisitor { cli: self.cli }) + } +} + +// New deserializer for SetGroups that returns values instead of using &mut +struct SetGroupsDeserializerReturn<'a> { + cli: &'a Cli, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for SetGroupsDeserializerReturn<'a> { + type Value = (Option>, Option, bool); + #[allow(clippy::too_many_lines)] + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "f")] + Fallback, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub")] + Del, + } + struct SGroupsChooserVisitor<'a> { + cli: &'a Cli, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for SGroupsChooserVisitor<'a> { + type Value = (Option>, Option, bool); + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("SGroups structure") + } + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_borrowed_str"); + let group: DGroupType<'_> = v + .parse::() + .map_or_else(|_| v.into(), std::convert::Into::into); + let score = Some(SetgidMin::from(&group)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()) + && y.len() == 1 + && y[0] + != group + .fetch_id() + .ok_or_else(|| serde::de::Error::custom("Group does not exist"))? + { + return Ok((None, None, false)); + } + Ok((Some(DGroups::Single(group)), score, ok)) + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_str"); + self.visit_string(v.to_string()) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_string"); + let group: DGroupType<'_> = v + .parse::() + .map_or_else(|_| Cow::::from(v).into(), std::convert::Into::into); + let score = Some(SetgidMin::from(&group)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()) + && y.len() == 1 + && y[0] + != group + .fetch_id() + .ok_or_else(|| serde::de::Error::custom("Group does not exist"))? + { + return Ok((None, None, false)); + } + Ok((Some(DGroups::Single(group)), score, ok)) + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + debug!("SGroupsChooserVisitor: visit_u64"); + let group: DGroupType<'_> = >::from( + u32::try_from(v).map_err(|_| serde::de::Error::custom("Group id too large"))?, + ); + let score = Some(SetgidMin::from(&group)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()) + && y.len() == 1 + && y[0] + != group + .fetch_id() + .ok_or_else(|| serde::de::Error::custom("Group does not exist"))? + { + return Ok((None, None, false)); + } + Ok((Some(DGroups::Single(group)), score, ok)) + } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + debug!("SGroupsChooserVisitor: visit_seq"); + let mut groups = None; + let mut score = None; + let mut ok = false; + let filter = self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()); + while let Some(group) = seq.next_element::()? { + if let Some(u) = filter { + let parsed_ids: Vec = + (&group).try_into().map_err(serde::de::Error::custom)?; + if *u == parsed_ids { + ok = true; + groups = Some(group.clone()); + score.replace((&group).into()); + while seq.next_element::()?.is_some() {} + break; + } + } else { + groups = Some(group.clone()); + ok = true; + score.replace((&group).into()); + } + } + Ok((groups, score, ok)) + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut groups = None; + let mut score = None; + let mut ok = false; + let filter = self.cli.opt_filter.as_ref().and_then(|x| x.group.as_ref()); + 'fields: while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("SGroupsChooserVisitor: default"); + let default = map.next_value::()?; + if default.is_all() { + ok = true; + } + } + Field::Fallback => { + debug!("SGroupsChooserVisitor: fallback"); + let value = map.next_value::()?; + if let Some(u) = filter { + let parsed_ids: Vec = + (&value).try_into().map_err(serde::de::Error::custom)?; + if *u == parsed_ids { + ok = true; + groups = Some(value.clone()); + score.replace((&value).into()); + } + } else { + groups = Some(value.clone()); + ok = true; + score.replace((&value).into()); + } + } + Field::Add => { + debug!("SGroupsChooserVisitor: add"); + if let Some(filter) = filter { + let add = map.next_value::>()?; + for group in add.iter() { + let v: Vec = + group.try_into().map_err(serde::de::Error::custom)?; + if v == *filter { + ok = true; + groups = Some(group.to_owned()); + score.replace(group.into()); + while map.next_entry::()?.is_some() + { + } + break; + } + } + } else { + map.next_value::()?; + } + } + Field::Del => { + debug!("SGroupsChooserVisitor: del"); + if let Some(u) = filter { + for group in map.next_value::>()?.iter() { + if let Ok(v) = TryInto::>::try_into(group) { + if v == *u { + while map + .next_entry::()? + .is_some() + { + } + ok = false; + groups = None; + score = None; + break 'fields; + } + } else { + return Err(serde::de::Error::custom("Invalid group")); + } + } + } else { + map.next_value::()?; + } + } + } + } + Ok((groups, score, ok)) + } + } + deserializer.deserialize_any(SGroupsChooserVisitor { cli: self.cli }) + } +} + +// New deserializer for SetUser that returns values instead of using &mut +struct SetUserDeserializerReturn<'a> { + cli: &'a Cli, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for SetUserDeserializerReturn<'a> { + type Value = (Option>, Option, bool); + #[allow(clippy::too_many_lines)] + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "d")] + Default, + #[serde(alias = "f")] + Fallback, + #[serde(alias = "a")] + Add, + #[serde(alias = "s", alias = "sub")] + Del, + } + struct SetUserVisitor<'a> { + cli: &'a Cli, + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for SetUserVisitor<'a> { + type Value = (Option>, Option, bool); + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("SUser structure") + } + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_borrowed_str"); + let user = v + .parse::() + .map_or_else(|_| DUserType::from(v), DUserType::from); + let score = Some(SetuidMin::from(&user)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.user) + && *y + != user + .fetch_id() + .ok_or_else(|| serde::de::Error::custom("User does not exist"))? + { + return Ok((None, None, false)); + } + Ok((Some(user), score, ok)) + } + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_str"); + self.visit_string(v.to_string()) + } + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_string"); + let user = v + .parse::() + .map_or_else(|_| DUserType::from(v), DUserType::from); + let score = Some(SetuidMin::from(&user)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.user) + && *y + != user + .fetch_id() + .ok_or_else(|| serde::de::Error::custom("User does not exist"))? + { + return Ok((None, None, false)); + } + Ok((Some(user), score, ok)) + } + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + debug!("SetUserVisitor: visit_i64"); + let user = DUserType::from( + u32::try_from(v).map_err(|_| serde::de::Error::custom("User id too large"))?, + ); + let score = Some(SetuidMin::from(&user)); + let ok = true; + if let Some(y) = &self.cli.opt_filter.as_ref().and_then(|x| x.user) + && *y + != user + .fetch_id() + .ok_or_else(|| serde::de::Error::custom("User does not exist"))? + { + return Ok((None, None, false)); + } + Ok((Some(user), score, ok)) + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut user = None; + let mut score = None; + let mut ok = false; + let filter = self.cli.opt_filter.as_ref().and_then(|x| x.user.as_ref()); + 'fields: while let Some(key) = map.next_key()? { + match key { + Field::Default => { + debug!("SUserChooserVisitor: default"); + let default = map.next_value::()?; + if default.is_all() { + ok = true; + } + } + Field::Fallback => { + debug!("SUserChooserVisitor: fallback"); + let value = map.next_value::()?; + if let Some(u) = filter { + let userid = value.fetch_id().ok_or_else(|| { + serde::de::Error::custom("User does not exist") + })?; + if u == &userid { + score.replace((&value).into()); + user = Some(value); + ok = true; + } + } else { + ok = true; + score.replace((&value).into()); + user = Some(value); + } + } + Field::Add => { + debug!("SUserChooserVisitor: add"); + if let Some(filter) = filter { + let users = map.next_value::>()?; + for user_item in users.iter() { + let user_id = user_item.fetch_id().ok_or_else(|| { + serde::de::Error::custom("User does not exist") + })?; + if user_id == *filter { + ok = true; + user = Some(user_item.to_owned()); + score.replace(user_item.into()); + break; + } + } + } else { + map.next_value::()?; + } + } + Field::Del => { + debug!("SUserChooserVisitor: del"); + if let Some(u) = filter { + let users = map.next_value::>()?; + for user_item in users.iter() { + let user_id = user_item.fetch_id().ok_or_else(|| { + serde::de::Error::custom("User does not exist") + })?; + if user_id == *u { + while map.next_entry::()?.is_some() + { + } + score = None; + user = None; + ok = false; + break 'fields; + } + } + } else { + map.next_value::()?; + } + } + } + } + Ok((user, score, ok)) + } + } + deserializer.deserialize_any(SetUserVisitor { cli: self.cli }) + } +} + +#[cfg(test)] +mod test { + + use crate::finder::de::tests::{get_non_root_gid, get_non_root_uid}; + + use super::*; + use capctl::Cap; + use rar_common::database::{ + FilterMatcher, + actor::{DGroupType, SGroupType}, + score::{SetgidMin, SetuidMin}, + }; + use test_log::test; + + #[test] + fn test_setuserdeserializerreturn() { + let json = + r#"{"default": "none", "fallback": "user1", "add": ["user2"], "del": ["user3"]}"#; + let cli = Cli::builder().build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from("user1"); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + } + + #[test] + fn test_setuserdeserializerreturn_filter() { + let uid1 = get_non_root_uid(0).unwrap(); + let uid2 = get_non_root_uid(1).unwrap(); + let json = format!( + r#"{{"default": "none", "fallback": "root", "add": [{uid1}], "del": [{uid2}]}}"# + ); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from(uid1); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from("root"); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid2).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(user, None); + let json = "\"root\""; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from("root"); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(user, None); + let json = "0"; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user(uid1).unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(user, None); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().user("root").unwrap().build()) + .build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + let user1 = DUserType::from(0); + assert_eq!(score, Some(SetuidMin::from(&user1))); + assert_eq!(user, Some(user1)); + } + + #[test] + fn test_no_fallback() { + let json = r#"{"default": "all"}"#; + let cli = Cli::builder().build(); + let deserializer = SetUserDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (user, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(score, None); + assert_eq!(user, None); + } + + #[test] + fn test_setgroupsdeserializerreturn() { + let json = r#"{"default": "none", "fallback": [1, 2], "add": [[3, 4]], "del": [[5, 6]]}"#; + let cli = Cli::builder().build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::from(vec![1.into(), 2.into()]); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + } + + #[test] + fn test_setgroupsdeserializerreturn_filter() { + let gid1 = get_non_root_gid(0).unwrap(); + let gid2 = get_non_root_gid(1).unwrap(); + let json = format!( + r#"{{"default": "none", "fallback": ["root"], "add": [[{gid1}]], "del": [[{gid2}]]}}"# + ); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single("root".into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single(gid1.into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid2).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + let json = "\"root\""; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single("root".into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + let json = "0"; + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group("root").unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::Single(0.into()); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let json = "[[\"root\", 1]]"; + let cli = Cli::builder() + .opt_filter( + FilterMatcher::builder() + .group(vec!["root".into(), Into::::into(1)]) + .unwrap() + .build(), + ) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::from(vec!["root".into(), 1.into()]); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + let groups1 = DGroups::from(vec!["root".into(), 1.into()]); + assert_eq!(score, Some((&groups1).into())); + assert_eq!(groups, Some(groups1)); + let cli = Cli::builder() + .opt_filter(FilterMatcher::builder().group(gid1).unwrap().build()) + .build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(!ok); + assert_eq!(score, None); + assert_eq!(groups, None); + } + + #[test] + fn test_no_fallback_groups() { + let json = r#"{"default": "all"}"#; + let cli = Cli::builder().build(); + let deserializer = SetGroupsDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok()); + let (groups, score, ok) = result.unwrap(); + assert!(ok); + assert_eq!(score, None); + assert_eq!(groups, None); + } + + #[test] + fn test_cred_deserializer() { + let json = r#"{"setuid":"root", "setgid":"root", "caps": ["CAP_SYS_ADMIN"]}"#; + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let result = result.unwrap(); + assert!(result.ok); + assert_eq!(result.cred.setuid, Some("root".into())); + assert_eq!( + result.cred.setgroups, + Some(DGroups::from(vec!["root".into()])) + ); + assert_eq!( + result.cred.caps, + Some(CapSet::from_iter(vec![Cap::SYS_ADMIN])) + ); + assert_eq!( + result.score.setuser_min.uid, + Some(SetuidMin::from(&"root".into())) + ); + assert_eq!( + result.score.setuser_min.gid, + Some(SetgidMin::from(&Into::>::into("root"))) + ); + assert_eq!(result.score.caps_min, CapsMin::CapsAdmin(1)); + + let uid = get_non_root_uid(0).unwrap(); + let gid = get_non_root_gid(0).unwrap(); + let json = format!(r#"{{"setuid":{uid}, "setgid":[[{gid}]]}}"#); + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let result = result.unwrap(); + assert!(result.ok); + assert_eq!(result.cred.setuid, Some(uid.into())); + assert_eq!(result.cred.setgroups, Some(DGroups::from(vec![gid.into()]))); + assert_eq!(result.cred.caps, None); + assert_eq!( + result.score.setuser_min.uid, + Some(SetuidMin::from(&uid.into())) + ); + assert_eq!( + result.score.setuser_min.gid, + Some(SetgidMin::from(&Into::>::into(uid))) + ); + assert_eq!(result.score.caps_min, CapsMin::Undefined); + + let uid = get_non_root_uid(0).unwrap(); + let gid = get_non_root_gid(0).unwrap(); + let json = format!(r#"{{"setuid":"{uid}", "setgid":["{gid}"]}}"#); + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let result = result.unwrap(); + assert!(result.ok); + assert_eq!(result.cred.setuid, Some(uid.into())); + assert_eq!(result.cred.setgroups, Some(DGroups::from(vec![gid.into()]))); + assert_eq!(result.cred.caps, None); + assert_eq!( + result.score.setuser_min.uid, + Some(SetuidMin::from(&uid.into())) + ); + assert_eq!( + result.score.setuser_min.gid, + Some(SetgidMin::from(&Into::>::into(uid))) + ); + assert_eq!(result.score.caps_min, CapsMin::Undefined); + } + + #[test] + fn test_cred_deserializer_invalid() { + let json = r#"{"setuid":-1, "setgid":"invalid", "caps": ["CAP_SYS_ADMIN"]}"#; + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + let json = r#"{"setuid":"invalid", "setgid":-1, "caps": ["CAP_SYS_ADMIN"]}"#; + let cli = Cli::builder().build(); + let deserializer = CredFinderDeserializerReturn { cli: &cli }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + } + + #[test] + fn test_expecting_error() { + let seq = "[1, 2, 3]"; + let float = "1.0"; + let cli = Cli::builder().build(); + let cred = CredFinderDeserializerReturn { cli: &cli }; + let result = cred.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + let setuser = SetUserDeserializerReturn { cli: &cli }; + let result = setuser.deserialize(&mut serde_json::Deserializer::from_str(float)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + let setgroups = SetGroupsDeserializerReturn { cli: &cli }; + let result = setgroups.deserialize(&mut serde_json::Deserializer::from_str(float)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + } +} diff --git a/src/sr/finder/de/mod.rs b/src/sr/finder/de/mod.rs new file mode 100644 index 00000000..e20e995b --- /dev/null +++ b/src/sr/finder/de/mod.rs @@ -0,0 +1,1044 @@ +use std::{borrow::Cow, collections::HashMap, fmt::Display, ops::Deref, path::PathBuf}; + +use bon::Builder; +use derivative::Derivative; +use log::debug; +use rar_common::{ + Cred, + database::{ + score::{ActorMatchMin, CmdMin, Score, SecurityMin, TaskScore}, + structs::SetBehavior, + }, + util::StorageMethod, +}; +use serde::{ + Deserialize, + de::{DeserializeSeed, IgnoredAny, Visitor}, +}; +use serde_json::Value; +use strum::EnumIs; + +use crate::{ + Cli, + finder::{ + de::{cred::CredData, roles::RoleListFinderDeserializer}, + options::DPathOptions, + }, +}; + +use super::options::Opt; + +pub mod commands; +pub mod cred; +//Coming soon: options can be optimized way more than today. +//pub mod opt; +pub mod roles; +pub mod settings; +pub mod tasks; + +#[cfg_attr(test, derive(Builder))] +#[derive(PartialEq, Eq, Debug, Default)] +pub struct DConfigFinder<'a> { + pub options: Option>, + pub roles: Vec>, +} + +#[cfg_attr(test, derive(Builder))] +#[derive(Debug, Derivative)] +#[derivative(PartialEq, Eq)] +pub struct DRoleFinder<'a> { + #[cfg_attr(test, builder(default))] + pub user_min: ActorMatchMin, + #[cfg_attr(test, builder(into))] + pub role: Cow<'a, str>, + #[cfg_attr(test, builder(default))] + pub tasks: Vec>, + pub options: Option>, + #[cfg_attr(test, builder(default))] + pub extra_values: HashMap, Value>, +} + +#[derive(Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] +#[serde(untagged)] +pub enum IdTask<'a> { + Name(#[serde(borrow)] Cow<'a, str>), + Number(usize), +} + +impl Display for IdTask<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IdTask::Name(name) => write!(f, "{name}"), + IdTask::Number(num) => write!(f, "{num}"), + } + } +} + +#[derive(Debug, Derivative, Builder)] +#[derivative(PartialEq, Eq)] +pub struct DTaskFinder<'a> { + pub id: IdTask<'a>, + #[builder(default)] + pub score: TaskScore, + pub cred: CredData<'a>, + pub commands: Option>, + pub options: Option>, + pub final_path: Option, +} + +/// This struct keeps the list of commands because options may be written after +#[cfg_attr(test, derive(Builder))] +#[derive(PartialEq, Eq, Debug)] +pub struct DCommandList<'a> { + #[cfg_attr(test, builder(start_fn, into))] + pub default_behavior: Option, + #[cfg_attr(test, builder(default, into))] + pub add: Cow<'a, [DCommand<'a>]>, + #[cfg_attr(test, builder(default, into))] + pub del: Cow<'a, [DCommand<'a>]>, +} + +#[derive(Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] +#[serde(untagged)] +pub enum DCommand<'a> { + Simple(#[serde(borrow)] Cow<'a, str>), + Complex(Value), +} + +#[cfg(test)] +impl<'a> DCommand<'a> { + pub fn simple(cmd: &'a str) -> Self { + DCommand::Simple(Cow::Borrowed(cmd)) + } + pub fn complex(cmd: Value) -> Self { + DCommand::Complex(cmd) + } +} + +/// This is clearer for me to understanf what type is ``is_human_readable`` +#[inline] +const fn to_storage_m(is_human_readable: bool) -> StorageMethod { + if is_human_readable { + StorageMethod::JSON + } else { + StorageMethod::CBOR + } +} + +impl<'a> DConfigFinder<'a> { + pub fn roles<'s>(&'s self) -> impl Iterator> { + self.roles.iter().map(|role| DLinkedRole::new(self, role)) + } + + #[cfg(any(feature = "hierarchy", feature = "ssd"))] + pub fn role<'s>(&'s self, role_name: &str) -> Option> { + self.roles + .iter() + .find(|r| r.role == role_name) + .map(|role| DLinkedRole::new(self, role)) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct DLinkedRole<'c, 'a> { + parent: &'c DConfigFinder<'a>, + role: &'c DRoleFinder<'a>, +} + +impl<'c, 'a> DLinkedRole<'c, 'a> { + const fn new(parent: &'c DConfigFinder<'a>, role: &'c DRoleFinder<'a>) -> Self { + Self { parent, role } + } + + pub fn tasks<'t>(&'t self) -> impl Iterator> { + self.role + .tasks + .iter() + .map(|task| DLinkedTask::new(self, task)) + } + + pub const fn role(&self) -> &DRoleFinder<'a> { + self.role + } + + pub const fn config(&self) -> &DConfigFinder<'a> { + self.parent + } +} + +#[derive(Clone, Copy, Debug)] +pub struct DLinkedTask<'t, 'c, 'a> { + parent: &'t DLinkedRole<'c, 'a>, + pub task: &'t DTaskFinder<'a>, +} + +impl<'t, 'c, 'a> DLinkedTask<'t, 'c, 'a> { + const fn new(parent: &'t DLinkedRole<'c, 'a>, task: &'t DTaskFinder<'a>) -> Self { + Self { parent, task } + } + + pub fn commands<'l>(&'l self) -> Option> { + self.task + .commands + .as_ref() + .map(|list| DLinkedCommandList::new(self, list)) + } + + pub const fn role(&self) -> &DLinkedRole<'c, 'a> { + self.parent + } + + pub const fn task(&self) -> &DTaskFinder<'a> { + self.task + } + + pub fn score(&self, cmd_min: CmdMin, security_min: SecurityMin) -> Score { + Score::builder() + .user_min(self.role().role.user_min) + .caps_min(self.score.caps_min) + .cmd_min(cmd_min) + .security_min(security_min) + .setuser_min(self.score.setuser_min) + .build() + } +} + +impl<'a> Deref for DLinkedTask<'_, '_, 'a> { + type Target = DTaskFinder<'a>; + fn deref(&self) -> &Self::Target { + self.task + } +} + +pub struct DLinkedCommandList<'l, 't, 'c, 'a> { + #[allow(dead_code)] // TODO: remove this + parent: &'l DLinkedTask<'t, 'c, 'a>, + command_list: &'l DCommandList<'a>, +} + +impl<'l, 't, 'c, 'a> DLinkedCommandList<'l, 't, 'c, 'a> { + const fn new(parent: &'l DLinkedTask<'t, 'c, 'a>, list: &'l DCommandList<'a>) -> Self { + Self { + parent, + command_list: list, + } + } + + pub fn add<'d>(&'d self) -> impl Iterator> { + self.command_list + .add + .iter() + .map(|cmd| DLinkedCommand::new(self, cmd)) + } + + pub fn del<'d>(&'d self) -> impl Iterator> { + self.command_list + .del + .iter() + .map(|cmd| DLinkedCommand::new(self, cmd)) + } +} + +impl<'a> Deref for DLinkedCommandList<'_, '_, '_, 'a> { + type Target = DCommandList<'a>; + fn deref(&self) -> &Self::Target { + self.command_list + } +} + +pub struct DLinkedCommand<'d, 'l, 't, 'c, 'a> { + #[allow(dead_code)] // TODO: remove this + parent: &'d DLinkedCommandList<'l, 't, 'c, 'a>, + pub command: &'d DCommand<'a>, +} + +impl<'d, 'l, 't, 'c, 'a> DLinkedCommand<'d, 'l, 't, 'c, 'a> { + const fn new( + parent: &'d DLinkedCommandList<'l, 't, 'c, 'a>, + command: &'d DCommand<'a>, + ) -> Self { + Self { parent, command } + } + + #[allow(dead_code)] // TODO: remove this + pub const fn task(&self) -> &DLinkedTask<'t, 'c, 'a> { + self.parent.parent + } +} + +impl<'a> Deref for DLinkedCommand<'_, '_, '_, '_, 'a> { + type Target = DCommand<'a>; + fn deref(&self) -> &Self::Target { + self.command + } +} + +/// This is the highly efficient deserializer +/// It is a lossy deserialiser, It skips information that is not matching the current user who is running the program +pub struct ConfigFinderDeserializer<'a> { + pub cli: &'a Cli, + pub cred: &'a Cred, + /// The current user path + pub env_path: &'a [&'a str], +} + +/// Let me explain a bit my deserialisation process +/// Here you get only ``Options``, ``Roles``. Options can arrive after Roles and vice-versa +/// In order to evaluate commands, you need PATH env var. +/// PATH var is defined in Options +/// So, we need to store Options for all the deserialisation process +/// (for Global case, other cases, see ``RoleFinderDeserializer``) +impl<'de: 'a, 'a> DeserializeSeed<'de> for ConfigFinderDeserializer<'a> { + type Value = DConfigFinder<'a>; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field<'a> { + #[serde(alias = "o")] + Options, + #[serde(alias = "r")] + Roles, + #[serde(untagged, borrow)] + #[allow(dead_code)] + Unknown(Cow<'a, str>), + } + + struct ConfigFinderVisitor<'a> { + cli: &'a Cli, + cred: &'a Cred, + env_path: &'a [&'a str], + } + + impl<'de: 'a, 'a> Visitor<'de> for ConfigFinderVisitor<'a> { + type Value = DConfigFinder<'a>; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("policy") + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + let mut options = None; + let mut roles = Vec::new(); + let mut spath = DPathOptions::default_path(); + while let Some(key) = map.next_key()? { + match key { + Field::Options => { + debug!("ConfigFinderVisitor: options"); + options = Some(map.next_value()?); + } + Field::Roles => { + debug!("ConfigFinderVisitor: roles"); + roles = map.next_value_seed(RoleListFinderDeserializer { + cli: self.cli, + cred: self.cred, + spath: &mut spath, + env_path: self.env_path, + })?; + } + Field::Unknown(_) => { + debug!("ConfigFinderVisitor: unknown"); + let _ = map.next_value::(); + } + } + } + Ok(DConfigFinder { options, roles }) + } + } + deserializer.deserialize_map(ConfigFinderVisitor { + cli: self.cli, + cred: self.cred, + env_path: self.env_path, + }) + } +} + +#[cfg(test)] +mod tests { + + use std::fs; + + use crate::finder::de::tasks::TaskListFinderDeserializer; + + use super::*; + use cbor4ii::core::utils::SliceReader; + use nix::unistd::{getgid, getuid}; + use rar_common::database::{ + actor::SGroups, + score::{CapsMin, SetUserMin, SetgidMin, SetuidMin}, + }; + use test_log::test; + + pub fn get_non_root_uid(nth: usize) -> Option { + // list all users + let passwd = fs::read_to_string("/etc/passwd").unwrap(); + let passwd: Vec<&str> = passwd.split('\n').collect(); + passwd + .iter() + .map(|line| { + let line: Vec<&str> = line.split(':').collect(); + line[2].parse::().unwrap() + }) + .filter(|uid| *uid != 0) + .nth(nth) + } + + pub fn get_non_root_gid(nth: usize) -> Option { + // list all users + let passwd = fs::read_to_string("/etc/group").unwrap(); + let passwd: Vec<&str> = passwd.split('\n').collect(); + passwd + .iter() + .map(|line| { + let line: Vec<&str> = line.split(':').collect(); + line[2].parse::().unwrap() + }) + .filter(|uid| *uid != 0) + .nth(nth) + } + + pub fn convert_json_to_cbor(json: &str) -> Vec { + let value: Value = serde_json::from_str(json).unwrap(); + + cbor4ii::serde::to_vec(Vec::new(), &value).unwrap() + } + + #[test] + fn test_idtask_display() { + let name = IdTask::Name(Cow::Borrowed("test")); + let number = IdTask::Number(42); + assert_eq!(format!("{name}"), "test"); + assert_eq!(format!("{number}"), "42"); + } + + #[test] + fn test_dcommandlist_deserialize_seq() { + let json = r#"["ls", "cat"]"#; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.add.len(), 2); + assert!(matches!(list.add[0], DCommand::Simple(_))); + } + + #[test] + fn test_dcommandlist_deserialize_map() { + let json = r#"{"default": "all", "add": ["ls"], "del": ["rm"]}"#; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior.unwrap(), SetBehavior::All); + assert_eq!(list.add.len(), 1); + assert_eq!(list.del.len(), 1); + } + + #[test] + fn test_dcommandlist_deserialize_all_or_none() { + let json = "\"all\""; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior, Some(SetBehavior::All)); + assert_eq!(list.add.len(), 0); + assert_eq!(list.del.len(), 0); + let json = "\"none\""; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior, Some(SetBehavior::None)); + assert_eq!(list.add.len(), 0); + assert_eq!(list.del.len(), 0); + } + + #[test] + fn test_dcommandlist_deserialize_empty() { + let json = "{}"; + let list: DCommandList = serde_json::from_str(json).unwrap(); + assert_eq!(list.default_behavior, None); + assert_eq!(list.add.len(), 0); + assert_eq!(list.del.len(), 0); + } + + #[test] + fn test_dcommandlist_deserialize_invalid() { + let json = r#"{"default": "invalid", "add": ["ls"], "del": ["rm"]}"#; + let result: Result = serde_json::from_str(json); + assert!(result.is_err()); + } + + #[test] + fn test_config_finder_deserializer() { + let json = format!( + r#"{{"roles":[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]}}"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let config = result.unwrap(); + assert_eq!(config.roles.len(), 1); + assert_eq!(config.roles[0].role, "r_test"); + } + + #[test] + fn test_config_finder_implementation() { + let json = format!( + r#"{{"roles":[{{"name":"r_test","actors":[{{"type":"user","id":{}}}],"tasks":[{{"name":"test","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/ls"]}},{{"name":"test2","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/ls","/usr/bin/cat"]}}]}},{{"name":"r_test2","actors":[{{"type":"group","names":[{}, {}]}}],"tasks":[{{"name":"test3","cred":{{"setuid":"0","setgid":["0",0],"caps":[]}},"commands":["/usr/bin/cat","/usr/bin/ls"]}}]}}]}}"#, + getuid().as_raw(), + getgid().as_raw(), + getgid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let config = result.unwrap(); + let mut roles = config.roles(); + let role_a = roles.next().unwrap(); + assert_eq!(role_a.role().role, "r_test"); + let mut tasks = role_a.tasks(); + let task_a = tasks.next().unwrap(); + assert_eq!(task_a.task().id, IdTask::Name("test".into())); + let commands = task_a.commands().unwrap(); + assert_eq!(commands.add().count(), 1); + assert_eq!( + *commands.add().next().unwrap().command, + DCommand::Simple("/usr/bin/ls".into()) + ); + let task_b = tasks.next().unwrap(); + assert_eq!(task_b.task().id, IdTask::Name("test2".into())); + let commands = task_b.commands().unwrap(); + assert_eq!(commands.add().count(), 2); + assert_eq!( + *commands.add().next().unwrap().command, + DCommand::Simple("/usr/bin/ls".into()) + ); + assert_eq!( + *commands.add().nth(1).unwrap().command, + DCommand::Simple("/usr/bin/cat".into()) + ); + assert!(tasks.next().is_none()); + let role_b = roles.next().unwrap(); + assert_eq!(role_b.role().role, "r_test2"); + let mut tasks = role_b.tasks(); + let task_a = tasks.next().unwrap(); + assert_eq!(task_a.task().id, IdTask::Name("test3".into())); + let commands = task_a.commands().unwrap(); + assert_eq!(commands.add().count(), 2); + assert_eq!( + *commands.add().next().unwrap().command, + DCommand::Simple("/usr/bin/cat".into()) + ); + assert_eq!( + *commands.add().nth(1).unwrap().command, + DCommand::Simple("/usr/bin/ls".into()) + ); + assert_eq!(commands.del().count(), 0); + assert!(tasks.next().is_none()); + assert!(roles.next().is_none()); + assert!(config.options.is_none()); + assert!(config.roles[0].options.is_none()); + assert!(config.roles[0].tasks[0].options.is_none()); + assert!(config.roles[0].tasks[1].options.is_none()); + assert!(config.roles[1].options.is_none()); + assert!(config.roles[1].tasks[0].options.is_none()); + assert!(config.role("r_test").is_some()); + assert!(config.role("r_test2").is_some()); + assert!(config.role("r_test3").is_none()); + assert_eq!(*config.role("r_test").unwrap().config(), config); + assert_eq!(*config.role("r_test2").unwrap().config(), config); + assert_eq!( + *config + .role("r_test") + .unwrap() + .tasks() + .next() + .unwrap() + .role(), + config.role("r_test").unwrap() + ); + assert_eq!( + *config + .role("r_test2") + .unwrap() + .tasks() + .next() + .unwrap() + .role(), + config.role("r_test2").unwrap() + ); + assert_eq!( + config + .role("r_test") + .unwrap() + .tasks() + .next() + .unwrap() + .score(CmdMin::MATCH, SecurityMin::empty()), + Score::builder() + .user_min(ActorMatchMin::UserMatch) + .setuser_min(SetUserMin { + uid: Some(SetuidMin::from(0)), + gid: Some(SetgidMin::from(SGroups::from(vec![0]))) + }) + .caps_min(CapsMin::NoCaps) + .security_min(SecurityMin::empty()) + .cmd_min(CmdMin::MATCH) + .build() + ); + } + + #[test] + fn test_config_with_options() { + let json = format!( + r#"{{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:05:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep1" + ], + "check": [ + "check1" + ], + "delete": [ + "del1" + ], + "set": {{ + "set1": "value1", + "set2": "value2" + }} + }}, + "root": "user", + "bounding": "strict" + }}, + "roles": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:06:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep2" + ], + "check": [ + "check2" + ], + "delete": [ + "del2" + ], + "set": {{ + "set1": "value2", + "set3": "value3" + }} + }}, + "root": "user", + "bounding": "strict" + }}, + "name": "role1", + "actors": [ + {{ + "type": "user", + "id": {} + }} + ], + "tasks": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:07:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep3" + ], + "check": [ + "check3" + ], + "delete": [ + "del3" + ], + "set": {{ + "set1": "value3", + "set4": "value4" + }} + }}, + "root": "user", + "bounding": "strict" + }}, + "name": "task1", + "cred": {{ + "setuid": 0, + "setgid": 0, + "caps": [ + "CAP_SYS_ADMIN", + "CAP_SYS_RESOURCE" + ] + }} + }} + ] + }} + ] +}}"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let config = result.unwrap(); + assert_eq!(config.roles.len(), 1); + assert_eq!(config.roles[0].role, "role1"); + assert_eq!(config.roles[0].tasks.len(), 1); + assert_eq!(config.roles[0].tasks[0].id, IdTask::Name("task1".into())); + assert!(config.options.is_some()); + assert!(config.roles[0].options.is_some()); + assert!(config.roles[0].tasks[0].options.is_some()); + } + + #[test] + fn test_config_optimized_with_options() { + let json = format!( + r#"{{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:05:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep1" + ], + "check": [ + "check1" + ], + "delete": [ + "del1" + ], + "set": {{ + "set1": "value1", + "set2": "value2" + }} + }}, + "root": "user", + "bounding": "strict" + }}, + "roles": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:06:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep2" + ], + "check": [ + "check2" + ], + "delete": [ + "del2" + ], + "set": {{ + "set1": "value2", + "set3": "value3" + }} + }}, + "root": "user", + "bounding": "strict" + }}, + "name": "role1", + "actors": [ + {{ + "type": "group", + "id": {} + }} + ], + "tasks": [ + {{ + "options": {{ + "timeout": {{ + "type": "ppid", + "duration": "00:07:00" + }}, + "path": {{ + "default": "delete", + "add": [ + "/usr/bin" + ] + }}, + "env": {{ + "default": "delete", + "override_behavior": false, + "keep": [ + "keep3" + ], + "check": [ + "check3" + ], + "delete": [ + "del3" + ], + "set": {{ + "set1": "value3", + "set4": "value4" + }} + }}, + "root": "user", + "bounding": "strict" + }}, + "name": "task1", + "cred": {{ + "setuid": 0, + "setgid": 0, + "caps": [ + "CAP_SYS_ADMIN", + "CAP_SYS_RESOURCE" + ] + }}, + "commands": ["/usr/bin/ls"] + }} + ] + }} + ] +}}"#, + getgid().as_raw() + ); + //convert json to cbor4ii + let cbor = convert_json_to_cbor(&json); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + }; + let result: Result, _> = deserializer.deserialize( + &mut cbor4ii::serde::Deserializer::new(SliceReader::new(cbor.as_slice())), + ); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let config = result.unwrap(); + assert_eq!(config.roles.len(), 1); + assert_eq!(config.roles[0].role, "role1"); + assert_eq!(config.roles[0].tasks.len(), 1); + assert_eq!(config.roles[0].tasks[0].id, IdTask::Name("task1".into())); + assert!(config.options.is_some()); + assert!(config.roles[0].options.is_some()); + assert!(config.roles[0].tasks[0].options.is_some()); + assert_eq!(config.roles[0].user_min, ActorMatchMin::GroupMatch(1)); + assert_eq!(config.roles[0].tasks[0].score.cmd_min, CmdMin::MATCH); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.uid, + Some(SetuidMin::from(&0.into())) + ); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.gid, + Some(SetgidMin::from(&vec![0])) + ); + assert_eq!( + config.roles[0].tasks[0].score.caps_min, + CapsMin::CapsAdmin(2) + ); + assert!(config.roles[0].tasks[0].commands.is_none()); + assert_eq!( + config.roles[0].tasks[0].final_path, + Some(PathBuf::from("/usr/bin/ls")) + ); + } + + #[test] + fn test_optimized_config() { + let uid = getuid().as_raw(); + let json = format!( + r#"{{"roles":[{{"name":"r_test","actors":[{{"type": "user", "id": {uid}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0"], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]}}"# + ); + //convert json to cbor4ii + let cbor = convert_json_to_cbor(&json); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + }; + let result: Result, _> = deserializer.deserialize( + &mut cbor4ii::serde::Deserializer::new(SliceReader::new(cbor.as_slice())), + ); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let config = result.unwrap(); + assert_eq!(config.roles[0].user_min, ActorMatchMin::UserMatch); + assert_eq!(config.roles[0].tasks[0].score.cmd_min, CmdMin::MATCH); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.uid, + Some(SetuidMin::from(&0.into())) + ); + assert_eq!( + config.roles[0].tasks[0].score.setuser_min.gid, + Some(SetgidMin::from(&vec![0])) + ); + assert_eq!(config.roles[0].tasks[0].score.caps_min, CapsMin::NoCaps); + assert!(config.roles[0].tasks[0].commands.is_none()); + assert_eq!( + config.roles[0].tasks[0].final_path, + Some(PathBuf::from("/usr/bin/ls")) + ); + } + + #[test] + fn test_expecting_error() { + let seq = "[1, 2, 3]"; + let map = "{\"1\": 2, \"3\": 4}"; + let cli = Cli::builder().build(); + let cred = Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(); + let config_finder = ConfigFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &cred, + }; + let result = config_finder.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + + let role_list = RoleListFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + spath: &mut DPathOptions::default(), + }; + let result = role_list.deserialize(&mut serde_json::Deserializer::from_str(map)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + let task_list = TaskListFinderDeserializer { + cli: &cli, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = task_list.deserialize(&mut serde_json::Deserializer::from_str(map)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + } + + // this test is to check if the deserializer can handle unknown types... It might evolve in the future + #[test] + fn test_unknown_type() { + let json = r#"{"unknown": "unknown"}"#; + let cli = Cli::builder().build(); + let deserializer = ConfigFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!( + result.is_ok(), + "Expected config with nothing in it, got: {result:?}" + ); + } +} diff --git a/src/sr/finder/de/opt.rs b/src/sr/finder/de/opt.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/sr/finder/de/opt.rs @@ -0,0 +1 @@ + diff --git a/src/sr/finder/de/roles.rs b/src/sr/finder/de/roles.rs new file mode 100644 index 00000000..a26973eb --- /dev/null +++ b/src/sr/finder/de/roles.rs @@ -0,0 +1,453 @@ +use std::{borrow::Cow, collections::HashMap}; + +use log::{debug, info}; +use nix::unistd::{Gid, Group}; +use rar_common::{ + Cred, + database::{ + actor::{DActor, DGroups}, + score::ActorMatchMin, + }, + util::{Either, StorageMethod}, +}; +use serde::{ + Deserialize, + de::{DeserializeSeed, IgnoredAny, Visitor}, +}; +use serde_json::Value; + +use crate::{ + Cli, + finder::{ + de::{DRoleFinder, DTaskFinder, tasks::TaskListFinderDeserializer, to_storage_m}, + options::DPathOptions, + }, +}; + +pub(super) struct RoleListFinderDeserializer<'a, 'b> { + pub(super) cli: &'a Cli, + pub(super) cred: &'a Cred, + /// spath is scoped only inside the deserialisation, useful for cbor + pub(super) spath: &'b mut DPathOptions<'a>, + /// The current user path + pub(super) env_path: &'a [&'a str], +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for RoleListFinderDeserializer<'a, '_> { + type Value = Vec>; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct RoleListFinderVisitor<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + spath: &'b mut DPathOptions<'a>, + env_path: &'a [&'a str], + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for RoleListFinderVisitor<'a, '_> { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("RoleList sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + debug!("RoleListFinderVisitor: visit_seq"); + let mut roles = Vec::new(); + while let Some(role) = seq.next_element_seed(RoleFinderDeserializer { + cli: self.cli, + cred: self.cred, + spath: self.spath, + env_path: self.env_path, + })? { + if let Some(role) = role { + debug!("adding role {role:?}"); + roles.push(role); + } + } + Ok(roles) + } + } + deserializer.deserialize_seq(RoleListFinderVisitor { + cli: self.cli, + cred: self.cred, + spath: self.spath, + env_path: self.env_path, + }) + } +} + +struct RoleFinderDeserializer<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + /// The current user path + env_path: &'a [&'a str], + /// spath is scoped only inside the deserialisation, useful for cbor + spath: &'b mut DPathOptions<'a>, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for RoleFinderDeserializer<'a, '_> { + type Value = Option>; + #[allow(clippy::too_many_lines)] + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field<'a> { + #[serde(alias = "n")] + Name, + #[serde(alias = "a", alias = "users")] + Actors, + #[serde(alias = "t")] + Tasks, + #[serde(alias = "o")] + Options, + #[serde(untagged, borrow)] + Unknown(Cow<'a, str>), + } + + struct RoleFinderVisitor<'a, 'b> { + cli: &'a Cli, + cred: &'a Cred, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, + // TODO: If perf problem in cbor you can trash this + #[allow(dead_code)] + policy_format: StorageMethod, + } + + impl<'de: 'a, 'a> Visitor<'de> for RoleFinderVisitor<'a, '_> { + type Value = Option>; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a role") + } + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + debug!("RoleFinderVisitor: visit_map"); + let mut role = None; + let mut tasks: Vec> = Vec::new(); + let mut options = None; + let mut extra_values = HashMap::new(); + let mut user_min = ActorMatchMin::default(); + while let Some(key) = map.next_key()? { + match key { + Field::Options => { + debug!("RoleFinderVisitor: options"); + options = Some(map.next_value()?); + } + Field::Name => { + debug!("RoleFinderVisitor: name"); + let role_name = map.next_value()?; + if self + .cli + .opt_filter + .as_ref() + .and_then(|x| x.role.as_ref()) + .is_some_and(|r| r != &role_name) + { + while map.next_entry::()?.is_some() {} + return Ok(None); + } + role = Some(role_name); + } + Field::Actors => { + debug!("RoleFinderVisitor: actors"); + user_min = + map.next_value_seed(ActorsFinderDeserializer { cred: self.cred })?; + } + Field::Tasks => { + debug!("RoleFinderVisitor: tasks"); + tasks = map.next_value_seed(TaskListFinderDeserializer { + cli: self.cli, + spath: self.spath, + env_path: self.env_path, + })?; + } + Field::Unknown(key) => { + debug!("RoleFinderVisitor: unknown {key}"); + let unknown: Value = map.next_value()?; + extra_values.insert(key, unknown); + } + } + } + Ok(Some(DRoleFinder { + user_min, + role: role.unwrap_or_default(), + tasks, + options, + extra_values, + })) + } + } + let human_readable = deserializer.is_human_readable(); + deserializer.deserialize_map(RoleFinderVisitor { + cli: self.cli, + cred: self.cred, + spath: self.spath, + env_path: self.env_path, + policy_format: to_storage_m(human_readable), + }) + } +} + +struct ActorsFinderDeserializer<'a> { + cred: &'a Cred, +} + +impl<'de> DeserializeSeed<'de> for ActorsFinderDeserializer<'_> { + type Value = ActorMatchMin; + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ActorsFinderVisitor<'a> { + cred: &'a Cred, + } + + impl<'de> Visitor<'de> for ActorsFinderVisitor<'_> { + type Value = ActorMatchMin; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a set of users") + } + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut user_matches = ActorMatchMin::NoMatch; + while let Some(actor) = seq.next_element::()? { + debug!("ActorsSettingsVisitor: actor {actor:?}"); + let temp = Self::user_matches(self.cred, &actor); + if temp != ActorMatchMin::NoMatch && temp < user_matches { + info!("ActorsSettingsVisitor: Better actor found {temp:?}"); + user_matches = temp; + } + } + Ok(user_matches) + } + } + + impl ActorsFinderVisitor<'_> { + fn match_groups(groups: &[Either], role_groups: &[&DGroups<'_>]) -> bool { + for role_group in role_groups { + if match role_group { + DGroups::Single(group) => groups.iter().any(|g| group == g), + DGroups::Multiple(multiple_actors) => multiple_actors + .iter() + .all(|actor| groups.iter().any(|g| actor == g)), + } { + return true; + } + } + false + } + fn user_matches(user: &Cred, actor: &DActor<'_>) -> ActorMatchMin { + match actor { + DActor::User { id, .. } => { + if *id == user.user { + return ActorMatchMin::UserMatch; + } + } + DActor::Group { groups, .. } => { + if Self::match_groups(&user.groups, &[groups]) { + return ActorMatchMin::GroupMatch(groups.len()); + } + } + DActor::Unknown(element) => { + unimplemented!("Unknown actor type: {:?}", element); + } + } + ActorMatchMin::NoMatch + } + } + + deserializer.deserialize_seq(ActorsFinderVisitor { cred: self.cred }) + } +} + +#[cfg(test)] +mod test { + use crate::finder::de::IdTask; + + use super::*; + use nix::unistd::{getgid, getuid}; + use test_log::test; + #[test] + fn test_actors_finder_deserializer() { + let json = format!(r#"[{{"type": "user", "id": {}}}]"#, getuid().as_raw()); + let deserializer = ActorsFinderDeserializer { + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let user_min = result.unwrap(); + assert_eq!(user_min, ActorMatchMin::UserMatch); + } + + #[test] + fn test_role_finder_deserializer() { + let json = format!( + r#"{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let role = result.unwrap().unwrap(); + assert_eq!(role.role, "r_test"); + assert_eq!(role.tasks.len(), 1); + assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); + } + + #[test] + fn test_role_list_finder_deserializer() { + let json = format!( + r#"[{{"name":"r_test","actors":[{{"type": "user", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"#, + getuid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleListFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let role = &result.unwrap()[0]; + assert_eq!(role.role, "r_test"); + assert_eq!(role.tasks.len(), 1); + assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); + let json = format!( + r#"[{{"name":"r_test","actors":[{{"type": "group", "id": {}}}], "tasks": [{{"name": "test", "cred": {{"setuid":"0", "setgid":["0", 0], "caps": []}}, "commands": ["/usr/bin/ls"]}}]}}]"#, + getgid().as_raw() + ); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleListFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let role = &result.unwrap()[0]; + assert_eq!(role.role, "r_test"); + assert_eq!(role.tasks.len(), 1); + assert_eq!(role.tasks[0].id, IdTask::Name("test".into())); + let json = r#"[{"name":"r_test","actors":[{"type": "user", "id": "874510"}], "tasks": [{"name": "test", "cred": {"setuid":"0", "setgid":["0", 0], "caps": []}, "commands": ["/usr/bin/ls"]}]}]"#.to_string(); + let cli = Cli::builder().cmd_path("ls").build(); + let deserializer = RoleListFinderDeserializer { + cli: &cli, + env_path: &["/usr/bin"], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(&json)); + assert!(result.is_ok(), "Failed to deserialize: {result:?}"); + let result = result.unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].user_min, ActorMatchMin::NoMatch); + } + + #[test] + fn test_expecting_errors() { + let int = "1"; + let cli = Cli::builder().build(); + let json = r#"{"unknown": "unknown"}"#; + let deserializer = RoleFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + spath: &mut DPathOptions::default(), + }; + let result = deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!( + result.is_ok(), + "Expected role with nothing in it, got: {result:?}" + ); + let actors = ActorsFinderDeserializer { + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + }; + let result = actors.deserialize(&mut serde_json::Deserializer::from_str(int)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + let role = RoleFinderDeserializer { + cli: &cli, + env_path: &[], + cred: &Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(), + spath: &mut DPathOptions::default(), + }; + let result = role.deserialize(&mut serde_json::Deserializer::from_str(int)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + } +} diff --git a/src/sr/finder/de/settings.rs b/src/sr/finder/de/settings.rs new file mode 100644 index 00000000..37f080bf --- /dev/null +++ b/src/sr/finder/de/settings.rs @@ -0,0 +1,121 @@ +use std::{ + io::{self, BufReader}, + path::Path, +}; + +use log::debug; +use rar_common::{ + SettingsContent, database::versionning::Versioning, file::LockedSettingsFile, + util::StorageMethod, +}; +use serde::{ + Deserialize, Deserializer, + de::{DeserializeSeed, IgnoredAny}, +}; + +pub type LockedStorage = LockedSettingsFile>; + +/// # Errors +/// Returns an error if the file cannot be opened, deserialized or locked +pub fn read_storage

( + rar_cfg_path: P, + rar_cfg_type: StorageMethod, +) -> std::io::Result +where + P: AsRef, +{ + LockedSettingsFile::open_read(rar_cfg_path, |path, file| { + debug!("Loading root settings from {}", path.as_ref().display()); + + let buf = BufReader::new(file); + let settings: Versioning = match rar_cfg_type { + StorageMethod::JSON => RootNoConfigDeserializer + .deserialize(&mut serde_json::Deserializer::from_reader(buf)) + .map_err(|e| { + debug!("Failed to deserialize root settings: {e}"); + io::Error::new(io::ErrorKind::InvalidData, e) + })?, + StorageMethod::CBOR => { + let mut io_reader = cbor4ii::core::utils::IoReader::new(buf); + RootNoConfigDeserializer + .deserialize(&mut cbor4ii::serde::Deserializer::new(&mut io_reader)) + .map_err(|e| { + debug!("Failed to deserialize root settings: {e}"); + io::Error::new(io::ErrorKind::InvalidData, e) + })? + } + }; + debug!("Loaded root settings from {}", path.as_ref().display()); + Ok(settings) + }) +} + +struct RootNoConfigDeserializer; + +impl<'de> DeserializeSeed<'de> for RootNoConfigDeserializer { + type Value = Versioning; + + /// # Errors + /// Returns an error if the deserialization process fails or if the "storage" field is missing + fn deserialize(self, deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct RootNoConfigVisitor; + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field { + #[serde(alias = "s")] + Storage, + #[serde(alias = "v")] + Version, + #[serde(other)] + Unknown, + } + impl<'de> serde::de::Visitor<'de> for RootNoConfigVisitor { + type Value = Versioning; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a SettingsContent") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut storage = None; + let mut version = None; + while let Some(key) = map.next_key::()? { + match key { + Field::Storage => { + if storage.is_some() { + return Err(serde::de::Error::duplicate_field("storage")); + } + storage = Some(map.next_value()?); + } + Field::Version => { + if version.is_some() { + return Err(serde::de::Error::duplicate_field("version")); + } + version = Some(map.next_value()?); + } + Field::Unknown => { + // Ignore unknown fields + let _ = map.next_value::()?; + } + } + } + + let storage = storage.ok_or_else(|| serde::de::Error::missing_field("storage"))?; + let version = version.ok_or_else(|| serde::de::Error::missing_field("version"))?; + + Ok(Versioning { + version, + data: storage, + }) + } + } + deserializer.deserialize_map(RootNoConfigVisitor) + } +} diff --git a/src/sr/finder/de/tasks.rs b/src/sr/finder/de/tasks.rs new file mode 100644 index 00000000..3d975ae5 --- /dev/null +++ b/src/sr/finder/de/tasks.rs @@ -0,0 +1,285 @@ +use std::borrow::Cow; + +use log::debug; +use rar_common::{database::score::TaskScore, util::StorageMethod}; +use serde::{ + Deserialize, + de::{DeserializeSeed, IgnoredAny}, +}; + +use crate::{ + Cli, + finder::{ + de::{ + DTaskFinder, IdTask, + commands::DCommandListDeserializer, + cred::{CredData, CredFinderDeserializerReturn}, + to_storage_m, + }, + options::DPathOptions, + }, +}; + +pub(super) struct TaskListFinderDeserializer<'a, 'b> { + pub(super) cli: &'a Cli, + /// The current user path + pub(super) env_path: &'a [&'a str], + /// spath is scoped only inside the deserialisation, useful for cbor + pub(super) spath: &'b mut DPathOptions<'a>, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for TaskListFinderDeserializer<'a, '_> { + type Value = Vec>; + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct TaskListFinderVisitor<'a, 'b> { + cli: &'a Cli, + spath: &'b mut DPathOptions<'a>, + env_path: &'a [&'a str], + } + impl<'de: 'a, 'a> serde::de::Visitor<'de> for TaskListFinderVisitor<'a, '_> { + type Value = Vec>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("TaskList sequence") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut tasks = Vec::new(); + let mut i = 0; + while let Some(element) = seq.next_element_seed(TaskFinderDeserializer { + cli: self.cli, + spath: self.spath, + env_path: self.env_path, + i, + })? { + if let Some(task) = element { + debug!("adding task {task:?}"); + tasks.push(task); + i += 1; + } + } + Ok(tasks) + } + } + deserializer.deserialize_seq(TaskListFinderVisitor { + cli: self.cli, + spath: self.spath, + env_path: self.env_path, + }) + } +} + +struct TaskFinderDeserializer<'a, 'b> { + cli: &'a Cli, + i: usize, + /// The current user path + env_path: &'a [&'a str], + /// spath is scoped only inside the deserialisation, useful for cbor + spath: &'b mut DPathOptions<'a>, +} + +impl<'de: 'a, 'a> DeserializeSeed<'de> for TaskFinderDeserializer<'a, '_> { + type Value = Option>; + + #[allow(clippy::too_many_lines)] + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + #[repr(u8)] + enum Field<'a> { + #[serde(alias = "n")] + Name, + #[serde(alias = "i", alias = "credentials")] + Cred, + #[serde(alias = "c", alias = "cmds")] + Commands, + #[serde(alias = "o")] + Options, + #[serde(untagged, borrow)] + Unknown(Cow<'a, str>), + } + + struct TaskFinderVisitor<'a, 'b> { + cli: &'a Cli, + i: usize, + env_path: &'a [&'a str], + spath: &'b mut DPathOptions<'a>, + storage_method: StorageMethod, + } + + impl<'de: 'a, 'a> serde::de::Visitor<'de> for TaskFinderVisitor<'a, '_> { + type Value = Option>; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("STask structure") + } + + fn visit_map(self, mut map: V) -> Result + where + V: serde::de::MapAccess<'de>, + { + // Use local temporaries for each field + let mut id = IdTask::Number(self.i); + let mut score = TaskScore::default(); + let mut commands = None; + let mut options = None; + let mut final_path = None; + //let mut extra_values = HashMap::new(); + let mut cred = CredData::default(); + + while let Some(key) = map.next_key()? { + match key { + Field::Options => { + debug!("TaskFinderVisitor: options"); + if let Some(opt) = map.next_value()? { + options = Some(opt); + } else { + while map.next_entry::()?.is_some() {} + return Ok(None); + } + + /*if self.cli.info && opt.execinfo.is_some_and(|i| i.is_hide()) { + while map.next_entry::()?.is_some() {} + return Ok(None); + } + // skip the task if env_override is required and not allowed + if self.cli.opt_filter.as_ref().is_some_and(|o| { + // we have a filter + o.env_behavior.as_ref().is_some_and(|_| { + // the filter overrides env behavior + opt.env.as_ref().is_some_and(|e| { + // the task specifies env options + e.override_behavior.is_some_and(|b| !b) // the task specifies override behavior and deny it + }) + }) + // in any other case, we cannot know if this task is valid or not (as we don't know the inherited env override value) + }) { + while map.next_entry::()?.is_some() {} + return Ok(None); + }*/ + } + Field::Name => { + debug!("TaskFinderVisitor: name"); + let task_name = map.next_value()?; + if self + .cli + .opt_filter + .as_ref() + .and_then(|x| x.task.as_ref()) + .is_some_and(|t| IdTask::Name(t.into()) != task_name) + { + while map.next_entry::()?.is_some() {} + return Ok(None); + } + id = task_name; + } + Field::Cred => { + debug!("TaskFinderVisitor: cred"); + let result = map + .next_value_seed(CredFinderDeserializerReturn { cli: self.cli })?; + if !result.ok { + while map.next_entry::()?.is_some() {} + return Ok(None); + } + cred = result.cred; + score.setuser_min = result.score.setuser_min; + score.caps_min = result.score.caps_min; + } + Field::Commands => { + debug!("TaskFinderVisitor: commands"); + // if json -> next_value (store) + // else -> next_value_seed -> use deserializer, thus highly optimizing + if self.storage_method.is_json() { + commands = Some(map.next_value()?); + } else { + map.next_value_seed(DCommandListDeserializer { + env_path: &self.spath.calc_path(self.env_path), + cmd_path: &self.cli.cmd_path, + cmd_args: &self.cli.cmd_args, + final_path: &mut final_path, + cmd_min: &mut score.cmd_min, + blocker: false, + })?; + } + } + Field::Unknown(_key) => { + debug!("TaskFinderVisitor: unknown"); + let _ = map.next_value::()?; + } + } + } + debug!("TaskFinderVisitor: final_path {final_path:?}"); + Ok(Some(DTaskFinder { + id, + score, + cred, + commands, + options, + final_path, + })) + } + } + let human_readable = deserializer.is_human_readable(); + deserializer.deserialize_map(TaskFinderVisitor { + i: self.i, + cli: self.cli, + env_path: self.env_path, + spath: self.spath, + storage_method: to_storage_m(human_readable), + }) + } +} + +#[cfg(test)] +mod test { + use serde::de::DeserializeSeed; + use test_log::test; + + use crate::{ + Cli, + finder::{ + de::{DCommandList, DTaskFinder, tasks::TaskFinderDeserializer}, + options::DPathOptions, + }, + }; + + #[test] + fn test_expecting_error() { + let seq = "[1, 2, 3]"; + let int = "1"; + let json = r#"{"unknown": "unknown"}"#; + let cli = Cli::builder().build(); + + let deserializer = TaskFinderDeserializer { + cli: &cli, + i: 0, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result: Result>, serde_json::Error> = + deserializer.deserialize(&mut serde_json::Deserializer::from_str(json)); + assert!( + result.is_ok(), + "Expected task with nothing in it, got: {result:?}" + ); + let task = TaskFinderDeserializer { + cli: &cli, + i: 0, + env_path: &[], + spath: &mut DPathOptions::default(), + }; + let result = task.deserialize(&mut serde_json::Deserializer::from_str(seq)); + assert!(result.is_err(), "Expected error, got: {result:?}"); + assert!(serde_json::from_str::(int).is_err()); + } +} diff --git a/src/sr/finder/mod.rs b/src/sr/finder/mod.rs index c39ec61e..4cbbb3fb 100644 --- a/src/sr/finder/mod.rs +++ b/src/sr/finder/mod.rs @@ -3,30 +3,34 @@ /// Only the settings that are needed are kept in memory use std::{ collections::HashMap, + fs, io::BufReader, path::{Path, PathBuf}, }; -use api::{Api, ApiEvent, register_plugins}; +use api::{Api, ApiEvent}; use bon::Builder; use de::{ConfigFinderDeserializer, DConfigFinder, DLinkedCommand, DLinkedRole, DLinkedTask}; use log::debug; use options::BorrowedOptStack; use rar_common::{ - Cred, StorageMethod, + Cred, database::{ actor::{DGroupType, DGroups}, - options::{SAuthentication, SBounding, SPrivileged, STimeout, SUMask}, - score::{CmdMin, CmdOrder, Score}, + options::{SAuthentication, SBounding, SPrivileged, STimeout, SUMask, WorkdirBehavior}, + score::{CmdMin, CmdOrder, HardenedBool, Score, hardened_bool_from_bool}, }, - util::{all_paths_from_env, read_with_privileges}, + util::{StorageMethod, WORKDIR_BEHAVIOR, all_paths_from_env, read_with_privileges}, }; use serde::de::DeserializeSeed; use crate::{ Cli, error::{SrError, SrResult}, - finder::de::CredOwnedData, + finder::{ + de::{cred::CredOwnedData, settings::read_storage}, + options::DWorkdirSet, + }, }; pub mod api; @@ -37,98 +41,167 @@ mod options; #[derive(Debug, Default, Clone, Builder)] pub struct BestExecSettings { #[builder(default)] + /// The final matching score. Evaluated over several fields, see `Score` pub score: Score, + #[builder(default)] + /// The execution path, canonalized and sanitized pub final_path: PathBuf, + #[builder(default)] + /// The Owned version of credentials needed for switching user and set capabilities pub cred: CredOwnedData, + + ///The task name matched in the policy pub task: Option, + #[builder(default)] + /// The role name matched in the policy pub role: String, #[builder(default)] + /// The final set of environment variable to keep/set pub env: HashMap, #[builder(default)] + /// The PATH variable is managed indepedently given the policy pub env_path: Vec, + /// The working directory to set, if specified in policy + pub workdir: Option, #[builder(default)] + /// Whether the Linux Capabilities are [bounded](https://www.man7.org/linux/man-pages/man7/capabilities.7.html) pub bounding: SBounding, #[builder(default)] + /// Information about whether the user should re-authenticate pub timeout: STimeout, #[builder(default)] + /// Information about whether the user should authenticate or bypass it pub auth: SAuthentication, #[builder(default)] + /// Is root id has it's privileges or not? If not, root is going to be a simple user pub root: SPrivileged, #[builder(default)] + /// Setting umask pub umask: SUMask, } +/// This functions is the main entrace to lookup at the security policy. +/// It efficiently check the policy based on the user args, skips unnecessary policy info etc. +/// The main focus here is to avoid at maximum any allocation. +/// # Returns +/// When a policy match was found, it returns all needed information, otherwise, it returns an ``SrError``. pub fn find_best_exec_settings<'de: 'a, 'a, P>( cli: &'a Cli, cred: &'a Cred, - path: &'a P, + rar_cfg_path: P, + rar_cfg_data_path: P, + rar_cfg_type: StorageMethod, env_vars: impl IntoIterator, impl Into)>, env_path: &[&str], ) -> SrResult where P: AsRef, { - register_plugins(); - let settings_file = rar_common::get_settings(path).map_err(|e| { - debug!("Policy unreachable: {e}"); - SrError::ConfigurationError - })?; - let config_finder_deserializer = ConfigFinderDeserializer { - cli, - cred, - env_path, - }; - match settings_file.storage.method { - StorageMethod::CBOR => { - let file_path = settings_file - .storage - .settings - .unwrap_or_default() - .path - .ok_or(SrError::ConfigurationError)?; - let file = read_with_privileges(&file_path)?; - let reader = BufReader::new(file); // Use BufReader for efficient streaming - let mut io_reader = cbor4ii::core::utils::IoReader::new(reader); // Use IoReader for streaming - Ok(BestExecSettings::retrieve_settings( - cli, - cred, - &config_finder_deserializer - .deserialize(&mut cbor4ii::serde::Deserializer::new(&mut io_reader)) - .map_err(|e| { - debug!("Error deserializing CBOR: {e}"); - SrError::ConfigurationError - })?, - env_vars, - env_path, - )?) + let env_vars: Vec<(String, String)> = env_vars + .into_iter() + .map(|(key, value)| (key.into(), value.into())) + .collect(); + let settings_file = read_storage(rar_cfg_path, rar_cfg_type)?; + let file_path = settings_file + .data + .data + .settings + .unwrap_or_default() + .path + .unwrap_or_else(|| rar_cfg_data_path.as_ref().to_path_buf()); + let parse_file = |file_path: &Path| -> SrResult { + let config_finder_deserializer = ConfigFinderDeserializer { + cli, + cred, + env_path, + }; + let file = read_with_privileges(file_path)?; + let reader = BufReader::new(file); + match settings_file.data.data.method { + StorageMethod::CBOR => { + let mut io_reader = cbor4ii::core::utils::IoReader::new(reader); + Ok(BestExecSettings::retrieve_settings( + cli, + cred, + &config_finder_deserializer + .deserialize(&mut cbor4ii::serde::Deserializer::new(&mut io_reader)) + .map_err(|e| { + debug!("Error deserializing CBOR: {e}"); + SrError::ConfigurationError + })?, + env_vars.iter().cloned(), + env_path, + )?) + } + StorageMethod::JSON => { + let io_reader = serde_json::de::IoRead::new(reader); + Ok(BestExecSettings::retrieve_settings( + cli, + cred, + &config_finder_deserializer + .deserialize(&mut serde_json::Deserializer::new(io_reader)) + .map_err(|e| { + debug!("Error deserializing JSON: {e}"); + SrError::ConfigurationError + })?, + env_vars.iter().cloned(), + env_path, + )?) + } } - StorageMethod::JSON => { - let file_path = settings_file - .storage - .settings - .unwrap_or_default() - .path - .ok_or(SrError::ConfigurationError)?; - let file = read_with_privileges(&file_path)?; - let reader = BufReader::new(file); - let io_reader = serde_json::de::IoRead::new(reader); - Ok(BestExecSettings::retrieve_settings( - cli, - cred, - &config_finder_deserializer - .deserialize(&mut serde_json::Deserializer::new(io_reader)) - .map_err(|e| { - debug!("Error deserializing JSON: {e}"); - SrError::ConfigurationError - })?, - env_vars, - env_path, - )?) + }; + + if file_path.is_dir() { + let mut entries: Vec = fs::read_dir(&file_path) + .map_err(|e| { + debug!( + "Failed to read policy directory {}: {e}", + file_path.display() + ); + SrError::ConfigurationError + })? + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.is_file()) + .collect(); + entries.sort(); + + let mut best: Option = None; + let mut parsed_any = false; + + for entry in entries { + let result = parse_file(&entry); + match result { + Ok(settings) => { + parsed_any = true; + if best.as_ref().is_none_or(|best_settings| { + settings.score.better_fully(&best_settings.score) + }) { + best = Some(settings); + } + } + Err(SrError::PermissionDenied) => { + parsed_any = true; + } + Err(err) => { + debug!("Skipping policy file {}: {err}", entry.display()); + } + } } + + return best.ok_or({ + if parsed_any { + SrError::PermissionDenied + } else { + SrError::ConfigurationError + } + }); } + + parse_file(&file_path) } impl BestExecSettings { @@ -143,9 +216,10 @@ impl BestExecSettings { let mut matching = false; let mut opt_stack = BorrowedOptStack::new(data.options.clone()); for role in data.roles() { - matching |= result.role_settings(cli, &role, &mut opt_stack, env_path)?; + matching |= result.role_settings(cli, cred, &role, &mut opt_stack, env_path)?; Api::notify(ApiEvent::BestRoleSettingsFound( cli, + cred, &role, &mut opt_stack, &env_path, @@ -178,12 +252,14 @@ impl BestExecSettings { result.timeout = opt_stack.calc_timeout(); result.root = opt_stack.calc_privileged(); result.umask = opt_stack.calc_umask(); + result.workdir = opt_stack.calc_workdir(); Ok(result) } pub fn role_settings<'c, 'a>( &mut self, cli: &'c Cli, + cred: &'c Cred, data: &DLinkedRole<'c, 'a>, opt_stack: &mut BorrowedOptStack<'a>, env_path: &[&str], @@ -194,7 +270,7 @@ impl BestExecSettings { } let mut res = false; for task in data.tasks() { - res |= self.task_settings(cli, &task, opt_stack, env_path)?; + res |= self.task_settings(cli, cred, &task, opt_stack, env_path)?; } Ok(res) } @@ -209,6 +285,7 @@ impl BestExecSettings { pub fn task_settings<'t, 'a>( &mut self, cli: &'t Cli, + cred: &'t Cred, data: &DLinkedTask<'t, '_, 'a>, opt_stack: &mut BorrowedOptStack<'a>, env_path: &[&str], @@ -229,6 +306,26 @@ impl BestExecSettings { ); return Ok(false); } + let result = temp_opt_stack.get_workdir_temp(); + if cli + .opt_filter + .as_ref() + .and_then(|f| f.workdir.as_ref()) + .is_some_and(|w| Self::allowed_workdir(&result, w.as_str()).is_false()) + { + debug!( + "task_settings: deny task due to the user wanted to execute to a specific folder, which do not match to settings" + ); + return Ok(false); + } + if Self::allowed_workdir(&result, cred.curdir.as_os_str().to_string_lossy().as_ref()) + .is_false() + { + debug!( + "task_settings: deny task due to the current directory do not match to allowed settings" + ); + return Ok(false); + } if cli.info && temp_opt_stack.calc_info().is_hide() { debug!("task_settings: deny task due to inherited from role or config info hide"); return Ok(false); @@ -355,6 +452,27 @@ impl BestExecSettings { }) } + /// This function takes a workdir path, and returns whether it is allowed based on the workdir settings. + fn allowed_workdir(result: &DWorkdirSet<'_>, workdir: &str) -> HardenedBool { + // Apply logic based on final behavior + match result.default_behavior { + WorkdirBehavior::Allowlist => { + // Block all except what is in "add" minus "sub" + let is_in_add = result.add.iter().any(|path| path.as_ref() == workdir); + let is_in_sub = result.sub.iter().any(|path| path.as_ref() == workdir); + hardened_bool_from_bool(is_in_add && !is_in_sub) + } + WorkdirBehavior::Blacklist => { + // Allow all except what is in "sub" + hardened_bool_from_bool(!result.sub.iter().any(|path| path.as_ref() == workdir)) + } + WorkdirBehavior::Inherit => { + // Should not happen after union, but default to WORKDIR_BEHAVIOR + hardened_bool_from_bool(WORKDIR_BEHAVIOR.is_blacklist()) + } + } + } + fn update_command_score(&mut self, final_path: PathBuf, res: CmdMin) -> bool { debug!( "update_command_score: current score {:?}, new score {:?}", @@ -385,9 +503,10 @@ mod tests { use std::path::PathBuf; use crate::Cli; - use crate::finder::de::CredData; + use crate::finder::de::cred::CredData; use crate::finder::options::{DEnvOptions, Opt}; use rar_common::Cred; + use test_log::test; // Helper: Dummy implementations for required traits/structs fn dummy_cli() -> Cli { @@ -398,7 +517,14 @@ mod tests { } fn dummy_cred() -> Cred { - Cred::builder().build() + Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build() } fn dummy_dconfigfinder<'a>() -> DConfigFinder<'a> { @@ -523,11 +649,12 @@ mod tests { fn test_role_settings_calls_actors_and_tasks() { let mut best = BestExecSettings::default(); let cli = dummy_cli(); + let cred = dummy_cred(); let binding = dummy_dconfigfinder(); let data = binding.roles().next().unwrap(); let mut opt_stack = BorrowedOptStack::new(None); let env_path = &["/bin"]; - let result = best.role_settings(&cli, &data, &mut opt_stack, env_path); + let result = best.role_settings(&cli, &cred, &data, &mut opt_stack, env_path); assert!(result.is_ok()); } @@ -545,12 +672,13 @@ mod tests { fn test_task_settings_sets_fields_on_found() { let mut best = BestExecSettings::default(); let cli = dummy_cli(); + let cred = dummy_cred(); let binding = dummy_dconfigfinder(); let binding = binding.roles().next().unwrap(); let data = binding.tasks().next().unwrap(); let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone()); let env_path = &["/bin"]; - let result = best.task_settings(&cli, &data, &mut opt_stack, env_path); + let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path); assert!(result.is_ok_and(|r| r)); assert!(*best.final_path == *"/usr/bin/ls"); assert!(best.role == "test"); @@ -630,6 +758,7 @@ mod tests { .cmd_args(vec!["-l".to_string()]) .info() .build(); + let cred = dummy_cred(); let binding = dummy_dconfigfinder(); let binding = binding.roles().next().unwrap(); let binding = binding.tasks().next().unwrap(); @@ -637,7 +766,7 @@ mod tests { let data = binding; let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone()); let env_path = &["/usr/bin"]; - let result = best.task_settings(&cli, &data, &mut opt_stack, env_path); + let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path); assert!(result.is_ok()); assert!(!result.unwrap()); } @@ -649,6 +778,7 @@ mod tests { .cmd_args(vec!["-l".to_string()]) .info() .build(); + let cred = dummy_cred(); // Now test with the second task which does not have info hide let binding = dummy_dconfigfinder(); let binding = binding.roles().next().unwrap(); @@ -657,7 +787,7 @@ mod tests { let data = binding; let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone()); let env_path = &["/usr/bin"]; - let result = best.task_settings(&cli, &data, &mut opt_stack, env_path); + let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path); assert!(result.is_ok()); assert!(result.unwrap()); } @@ -669,13 +799,14 @@ mod tests { .cmd_args(vec!["-l".to_string()]) .info() .build(); + let cred = dummy_cred(); // Now try best.role_settings to ensure full flow works let binding = dummy_dconfigfinder(); let binding = binding.roles().next().unwrap(); let data = binding.tasks().nth(1).unwrap(); let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone()); let env_path = &["/usr/bin"]; - let result = best.role_settings(&cli, &binding, &mut opt_stack, env_path); + let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path); assert!(result.is_ok()); assert!(result.unwrap()); assert!(*best.final_path == *"/usr/bin/ls"); @@ -690,12 +821,13 @@ mod tests { .cmd_args(vec!["-l".to_string()]) .info() .build(); + let cred = dummy_cred(); let binding = dummy_dconfigfinder(); let binding = binding.roles().nth(1).unwrap(); let data = binding.tasks().next().unwrap(); let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone()); let env_path = &["/usr/bin"]; - let result = best.role_settings(&cli, &binding, &mut opt_stack, env_path); + let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path); assert!(result.is_ok()); assert!(result.unwrap()); assert!(*best.final_path == *"/usr/bin/ls"); @@ -715,6 +847,7 @@ mod tests { .build(), ) .build(); + let cred = dummy_cred(); let binding = dummy_dconfigfinder(); let binding = binding.roles().next().unwrap(); let binding = binding.tasks().next().unwrap(); @@ -722,7 +855,7 @@ mod tests { let data = binding; let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone()); let env_path = &["/usr/bin"]; - let result = best.task_settings(&cli, &data, &mut opt_stack, env_path); + let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path); assert!(result.is_ok()); assert!(!result.unwrap()); } @@ -738,6 +871,7 @@ mod tests { .build(), ) .build(); + let cred = dummy_cred(); // Now test with the second task which does not have info hide let binding = dummy_dconfigfinder(); let binding = binding.roles().next().unwrap(); @@ -746,7 +880,7 @@ mod tests { let data = binding; let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone()); let env_path = &["/usr/bin"]; - let result = best.task_settings(&cli, &data, &mut opt_stack, env_path); + let result = best.task_settings(&cli, &cred, &data, &mut opt_stack, env_path); assert!(result.is_ok()); assert!(result.unwrap()); } @@ -762,13 +896,14 @@ mod tests { .build(), ) .build(); + let cred = dummy_cred(); // Now try best.role_settings to ensure full flow works let binding = dummy_dconfigfinder(); let binding = binding.roles().next().unwrap(); let data = binding.tasks().nth(1).unwrap(); let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone()); let env_path = &["/usr/bin"]; - let result = best.role_settings(&cli, &binding, &mut opt_stack, env_path); + let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path); assert!(result.is_ok()); assert!(result.unwrap()); assert!(*best.final_path == *"/usr/bin/ls"); @@ -787,12 +922,13 @@ mod tests { .build(), ) .build(); + let cred = dummy_cred(); let binding = dummy_dconfigfinder(); let binding = binding.roles().nth(1).unwrap(); let data = binding.tasks().next().unwrap(); let mut opt_stack = BorrowedOptStack::new(data.role().config().options.clone()); let env_path = &["/usr/bin"]; - let result = best.role_settings(&cli, &binding, &mut opt_stack, env_path); + let result = best.role_settings(&cli, &cred, &binding, &mut opt_stack, env_path); assert!(result.is_ok()); assert!(result.unwrap()); assert!(*best.final_path == *"/usr/bin/ls"); diff --git a/src/sr/finder/options.rs b/src/sr/finder/options.rs index a39dd9d2..6a15344e 100644 --- a/src/sr/finder/options.rs +++ b/src/sr/finder/options.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::path::PathBuf; use std::{borrow::Cow, collections::HashMap}; use bon::{Builder, bon, builder}; @@ -8,13 +9,14 @@ use nix::unistd::User; use rar_common::database::FilterMatcher; use rar_common::database::options::{ EnvBehavior, Level, PathBehavior, SAuthentication, SBounding, SInfo, SPathOptions, SPrivileged, - STimeout, SUMask, + STimeout, SUMask, WorkdirBehavior, }; use rar_common::database::score::SecurityMin; use rar_common::util::{ AUTHENTICATION, BOUNDING, ENV_CHECK_LIST, ENV_DEFAULT_BEHAVIOR, ENV_DELETE_LIST, ENV_KEEP_LIST, ENV_OVERRIDE_BEHAVIOR, ENV_PATH_ADD_LIST_SLICE, ENV_PATH_BEHAVIOR, ENV_PATH_REMOVE_LIST_SLICE, ENV_SET_LIST, INFO, PRIVILEGED, TIMEOUT_DURATION, TIMEOUT_MAX_USAGE, TIMEOUT_TYPE, UMASK, + WORKDIR_ADD_LIST, WORKDIR_BEHAVIOR, WORKDIR_FALLBACK, WORKDIR_REMOVE_LIST, }; use std::hash::Hash; @@ -73,23 +75,70 @@ pub struct DEnvOptions<'a> { pub delete: HashSet>, } -#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +#[serde(untagged)] +pub enum DWorkdirEither<'a> { + /// This is the equivalent of deny all and fallback to the specified path. + #[serde(borrow)] + Path(Cow<'a, str>), + #[serde(borrow)] + Struct(DWorkdirSet<'a>), +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)] +pub struct DWorkdirSet<'a> { + /// The default behavior for workdir handling. This determines how the "add" and "sub" lists are interpreted. + /// - If set to `Allowlist`, only the paths in the "add" list (minus those in the "sub" list) will be allowed as workdirs. + /// - If set to `Blacklist`, all paths will be allowed as workdirs except those in the "sub" list. + /// - If set to `Inherit`, the behavior will be inherited from parent levels, which can be combined with the above two behaviors. + /// + /// Note: The target user must have permissions to access the allowed workdirs, otherwise the command will fail to execute. + /// If you want bypass the access control check, grant the `CAP_DAC_READ_SEARCH` capability in the "cred" section + #[serde(rename = "default", default, skip_serializing_if = "is_default")] + #[builder(start_fn)] + pub default_behavior: WorkdirBehavior, + + /// The "fallback" field specifies a fallback directory to use as the working directory. + /// This will override the current user working directory. + /// For example: + /// someone type: `dosr ls` in his home directory, but the config has a fallback of `/tmp`, + /// then the command will be executed with `/tmp` as the working directory instead of the user's home directory. + /// This is useful in scenarios where users do not have to know or care about the actual working directory of a command + #[serde(borrow, default, skip_serializing_if = "Option::is_none")] + pub fallback: Option>, + + #[serde(borrow, default, skip_serializing_if = "HashSet::is_empty")] + #[builder(with = |v : impl IntoIterator>>| { v.into_iter().map(std::convert::Into::into).collect() })] + pub add: HashSet>, + #[serde( + borrow, + default, + skip_serializing_if = "HashSet::is_empty", + alias = "del" + )] + #[builder(with = |v : impl IntoIterator>>| { v.into_iter().map(std::convert::Into::into).collect() })] + pub sub: HashSet>, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)] #[serde(rename_all = "kebab-case")] pub struct Opt<'a> { #[serde(skip)] pub level: Level, - #[serde(borrow, skip_serializing_if = "Option::is_none")] + #[serde(borrow, default, skip_serializing_if = "Option::is_none")] pub path: Option>, - #[serde(borrow, skip_serializing_if = "Option::is_none")] + #[serde(borrow, default, skip_serializing_if = "Option::is_none")] pub env: Option>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub root: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub bounding: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub authentication: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub execinfo: Option, + #[serde(borrow, default, skip_serializing_if = "Option::is_none")] + pub workdir: Option>, // we only need to store the enforced workdir, if existing. #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -109,6 +158,7 @@ impl<'a> Opt<'a> { bounding: Option, authentication: Option, execinfo: Option, + workdir: Option>, timeout: Option, umask: Option, #[builder(default)] extra_fields: Value, @@ -121,6 +171,7 @@ impl<'a> Opt<'a> { bounding, authentication, execinfo, + workdir, timeout, umask, extra_fields, @@ -192,7 +243,11 @@ impl DEnvOptions<'_> { final_set.insert("RAR_UID".into(), current_user.user.uid.to_string()); final_set.insert("RAR_GID".into(), current_user.user.gid.to_string()); final_set.insert("RAR_USER".into(), current_user.user.name.clone()); - final_set.insert("RAR_COMMAND".into(), command); + final_set.insert("RAR_COMMAND".into(), command.clone()); + final_set.insert("SUDO_UID".into(), current_user.user.uid.to_string()); + final_set.insert("SUDO_GID".into(), current_user.user.gid.to_string()); + final_set.insert("SUDO_USER".into(), current_user.user.name.clone()); + final_set.insert("SUDO_COMMAND".into(), command); final_set .entry("TERM".into()) .or_insert_with(|| "unknown".into()); @@ -596,6 +651,7 @@ impl<'a, 'c, 't> BorrowedOptStack<'a> { overriden, default_behavior, ); + // we reset as long there is a new default behavior, we don't inherit if default_behavior.is_keep() || default_behavior.is_delete() { result.set.clear(); result.keep.clear(); @@ -637,10 +693,10 @@ impl<'a, 'c, 't> BorrowedOptStack<'a> { .result(&mut result) .overriden(&mut overriden) .default_behavior(ENV_DEFAULT_BEHAVIOR) - .keep(&ENV_KEEP_LIST) - .check(&ENV_CHECK_LIST) - .delete(&ENV_DELETE_LIST) - .set(&ENV_SET_LIST) + .keep(&ENV_KEEP_LIST.iter().copied()) + .check(&ENV_CHECK_LIST.iter().copied()) + .delete(&ENV_DELETE_LIST.iter().copied()) + .set(&ENV_SET_LIST.iter().copied()) .call(); self.get_opt_iter() .filter_map(|o| o.env.as_ref()) @@ -708,6 +764,77 @@ impl<'a, 'c, 't> BorrowedOptStack<'a> { .find_map(|o| o.umask) .unwrap_or(UMASK) } + pub fn calc_workdir(&self) -> Option { + self.get_opt_iter_rev() + .filter_map(|o| o.workdir.as_ref()) + .find_map(|w| match w { + DWorkdirEither::Path(p) => Some(p.as_ref().into()), + DWorkdirEither::Struct(s) => s.fallback.as_ref().map(|f| f.to_string().into()), + }) + } + pub fn get_workdir_temp(&self) -> DWorkdirSet<'_> { + #[builder] + #[allow(clippy::ref_option)] // Because builder do not handle it well. + fn assign_workdir_settings( + result: &mut DWorkdirSet<'_>, + default_behavior: WorkdirBehavior, + add: &(impl IntoIterator> + Clone), + del: &(impl IntoIterator> + Clone), + fallback: &Option>, + ) { + // we reset as long there is a new default behavior, we don't inherit + if default_behavior.is_allowlist() || default_behavior.is_blacklist() { + result.add.clear(); + result.sub.clear(); + result.fallback.take(); + } + result.add.extend( + add.clone() + .into_iter() + .map(|k| k.as_ref().to_string().into()), + ); + result.sub.extend( + del.clone() + .into_iter() + .map(|k| k.as_ref().to_string().into()), + ); + if let Some(fallback) = fallback.as_ref() { + result + .fallback + .replace(fallback.as_ref().to_string().into()); + } + } + let mut result = DWorkdirSet::default(); + assign_workdir_settings() + .result(&mut result) + .default_behavior(WORKDIR_BEHAVIOR) + .add(&WORKDIR_ADD_LIST) + .del(&WORKDIR_REMOVE_LIST) + .fallback(&WORKDIR_FALLBACK) + .call(); + self.get_opt_iter() + .filter_map(|o| o.workdir.as_ref()) + .for_each(|o| match o { + DWorkdirEither::Struct(s) => assign_workdir_settings() + .result(&mut result) + .default_behavior(s.default_behavior) + .add(&s.add) + .del(&s.sub) + .fallback(&s.fallback) + .call(), + DWorkdirEither::Path(p) => { + let array: [String; 0] = []; + assign_workdir_settings() + .result(&mut result) + .default_behavior(WorkdirBehavior::Allowlist) + .fallback(&Some(p)) + .add(&array) + .del(&array) + .call(); + } + }); + result + } } #[bon::builder] @@ -869,6 +996,7 @@ mod tests { } #[test] + #[allow(clippy::too_many_lines)] fn test_calc_env() { let env_options = DEnvOptions::builder(EnvBehavior::Delete) .set(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")]) @@ -886,7 +1014,14 @@ mod tests { ("VAR5", "VALUE5"), ]; let env_path = vec!["/usr/local/bin", "/usr/bin"]; - let target = Cred::builder().build(); + let target = Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(); let result = env_options.calc_final_env(env_vars, &env_path, &target, None, String::new()); assert!( result.is_ok(), @@ -927,7 +1062,14 @@ mod tests { ("VAR5", "VALUE5"), ]; let env_path = vec!["/usr/local/bin", "/usr/bin"]; - let target = Cred::builder().build(); + let target = Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(); let result = env_options.calc_final_env(env_vars, &env_path, &target, None, String::new()); assert!( result.is_ok(), @@ -969,7 +1111,14 @@ mod tests { ("VAR5", "VALUE5"), ]; let env_path = vec!["/usr/local/bin", "/usr/bin"]; - let target = Cred::builder().build(); + let target = Cred::builder() + .curdir() + .unwrap() + .groups() + .unwrap() + .user() + .unwrap() + .build(); let result = env_options.calc_final_env(env_vars, &env_path, &target, None, String::new()); assert!(result.is_err()); } diff --git a/src/sr/main.rs b/src/sr/main.rs index 843e24af..faad9566 100644 --- a/src/sr/main.rs +++ b/src/sr/main.rs @@ -33,12 +33,17 @@ use rar_common::util::{BOLD, RST, UNDERLINE, drop_effective, subsribe}; use crate::error::SrError; use crate::error::SrResult; -use crate::finder::de::CredOwnedData; +use crate::finder::de::cred::CredOwnedData; #[cfg(not(test))] -const ROOTASROLE: &str = env!("RAR_CFG_PATH"); +pub const RAR_CFG_PATH: &str = env!("RAR_CFG_PATH"); #[cfg(test)] -const ROOTASROLE: &str = "target/rootasrole.json"; +pub const RAR_CFG_PATH: &str = "target/rootasrole.json"; + +#[cfg(not(test))] +pub const RAR_CFG_DATA_PATH: &str = env!("RAR_CFG_DATA_PATH"); +#[cfg(test)] +pub const RAR_CFG_DATA_PATH: &str = "target/rootasrole.json"; //const ABOUT: &str = "Execute privileged commands with a role-based access control system"; //const LONG_ABOUT: &str = @@ -64,6 +69,9 @@ const USAGE: &str = formatcp!( {BOLD}-E, --preserve-env{RST} Preserve environment variables if allowed by a matching task + {BOLD}-D, --chdir {RST} + Workdir option allows you to specify the working directory for the command + {BOLD}-p, --prompt {RST} Prompt option allows you to override the default password prompt and use a custom one [default: "Password: "] @@ -81,10 +89,7 @@ const USAGE: &str = formatcp!( Print dosr version {BOLD}-h, --help{RST} - Print help (see a summary with '-h')"#, - UNDERLINE = UNDERLINE, - BOLD = BOLD, - RST = RST + Print help (see a summary with '-h')"# ); #[derive(Debug, Builder)] @@ -143,6 +148,7 @@ where let mut iter = s.into_iter().skip(1); let mut role = None; let mut task = None; + let mut workdir = None; let mut user: Option = None; let mut group: Option = None; let mut env = None; @@ -175,6 +181,9 @@ where "-E" | "--preserve-env" => { env.replace(EnvBehavior::Keep); } + "-D" | "--chdir" => { + workdir = iter.next().map(|s| escape_parser_string(s)); + } #[cfg(feature = "timeout")] "-K" | "--remove-timestamp" => { args.del_ts = true; @@ -211,6 +220,7 @@ where FilterMatcher::builder() .maybe_role(role) .maybe_task(task) + .maybe_workdir(workdir) .maybe_env_behavior(env) .maybe_user(user) .map_err(|e| { @@ -268,8 +278,13 @@ fn main() { fn main_inner() -> SrResult<()> { use std::env; - use crate::{ROOTASROLE, finder::api::Api, pam::start_session}; + use crate::{ + RAR_CFG_PATH, + finder::api::{Api, register_plugins}, + pam::start_session, + }; use finder::find_best_exec_settings; + use rar_common::util::RAR_CFG_TYPE; debug!("Started with capabilities: {:?}", CapState::get_current()?); drop_effective()?; @@ -284,7 +299,7 @@ fn main_inner() -> SrResult<()> { println!("{USAGE}"); return Ok(()); } - let user = make_cred(); + let user = make_cred().map_err(|_| SrError::SystemError)?; if args.del_ts { #[cfg(not(feature = "timeout"))] { @@ -302,10 +317,13 @@ fn main_inner() -> SrResult<()> { } } } + register_plugins(); let execcfg = find_best_exec_settings( &args, &user, - &ROOTASROLE.to_string(), + RAR_CFG_PATH, + RAR_CFG_DATA_PATH, + RAR_CFG_TYPE, env::vars(), env::var("PATH") .unwrap_or_default() @@ -400,6 +418,15 @@ fn main_inner() -> SrResult<()> { Api::notify(finder::api::ApiEvent::PreExec(&args, &execcfg))?; + // we set workdir if -D is provided + if let Some(workdir) = args.opt_filter.as_ref().and_then(|f| f.workdir.as_ref()) { + command_builder.current_dir(workdir); + // if no workdir from args, we check if exec settings enforce a workdir + } else if let Some(workdir) = execcfg.workdir.as_ref() { + command_builder.current_dir(workdir); + } + // otherwise we keep the same workdir + command_builder.args(cargs.iter()); command_builder.env_clear(); command_builder.envs(cfinal_env); @@ -427,8 +454,11 @@ fn main_inner() -> SrResult<()> { std::process::exit(status.code().unwrap_or(1)); } -fn make_cred() -> Cred { - Cred::builder() +fn make_cred() -> std::io::Result { + Ok(Cred::builder() + .user()? + .groups()? + .curdir()? .maybe_tty(stat::fstat(stdout()).ok().and_then(|s| { if isatty(stdout()).ok().unwrap_or(false) { Some(s.st_rdev) @@ -436,7 +466,7 @@ fn make_cred() -> Cred { None } })) - .build() + .build()) } fn set_capabilities(cred: &CredOwnedData, bounding: SBounding) -> SrResult<()> { @@ -525,7 +555,7 @@ mod tests { use crate::finder::BestExecSettings; - use super::finder::de::CredOwnedData; + use super::finder::de::cred::CredOwnedData; use capctl::{Cap, CapSet}; use libc::getgid; use nix::unistd::{Group, Pid, User, getgroups, getuid}; @@ -596,17 +626,21 @@ mod tests { #[test] fn test_make_cred() { - let user = make_cred(); - let gid = unsafe { getgid() }; + let user = make_cred().unwrap(); assert_eq!(user.user.uid, getuid()); - assert_eq!(user.user.gid.as_raw(), gid); let groups = getgroups() .unwrap() .iter() .map(|g| Group::from_gid(*g).unwrap().unwrap()) .collect::>(); assert!(!user.groups.is_empty()); - assert_eq!(user.groups, groups); + assert_eq!( + user.groups + .iter() + .map(|e| e.left().unwrap().clone()) + .collect::>(), + groups + ); assert_eq!(user.ppid, Pid::parent()); } diff --git a/src/sr/pam/mod.rs b/src/sr/pam/mod.rs index f182410a..017a5984 100644 --- a/src/sr/pam/mod.rs +++ b/src/sr/pam/mod.rs @@ -216,6 +216,12 @@ mod tests { Cred::builder() .maybe_tty(Some(0 as dev_t)) .ppid(Pid::from_raw(1)) + .groups() + .unwrap() + .user() + .unwrap() + .curdir() + .unwrap() .build() } diff --git a/src/sr/pre_exec.rs b/src/sr/pre_exec.rs index 9dbe076b..7d061d29 100644 --- a/src/sr/pre_exec.rs +++ b/src/sr/pre_exec.rs @@ -4,7 +4,7 @@ use nix::sys::stat::{Mode, umask}; use rar_common::{database::options::SBounding, util::activates_no_new_privs}; use rar_exec::orchestrator::{Orchestrator, PreExecContext, PreExecStep, Stage}; -use crate::finder::de::CredOwnedData; +use crate::finder::de::cred::CredOwnedData; struct PreExecConfig { umask: u16, @@ -122,7 +122,7 @@ pub static PRE_EXEC_ORCHESTRATOR: Orchestrator = Orchestrator::new(PRE_EXEC_STEP #[cfg(test)] mod tests { use super::*; - use crate::finder::de::CredOwnedData; + use crate::finder::de::cred::CredOwnedData; use rar_common::database::options::SBounding; #[test] diff --git a/src/sr/timeout.rs b/src/sr/timeout.rs index 5ddb9c00..317131cc 100644 --- a/src/sr/timeout.rs +++ b/src/sr/timeout.rs @@ -176,9 +176,10 @@ const TS_LOCATION: &str = env!("RAR_TIMEOUT_STORAGE"); const TS_LOCATION: &str = "target/ts"; fn read_cookies(user: &Cred) -> Result, Box> { - let path = Path::new(TS_LOCATION).join(user.user.uid.as_raw().to_string()); + let s_uid = user.user.uid.as_raw().to_string(); + let path = Path::new(TS_LOCATION).join(&s_uid); let lockpath = Path::new(TS_LOCATION) - .join(user.user.uid.as_raw().to_string()) // Convert u32 to String + .join(&s_uid) // Convert u32 to String .with_extension("lock"); if !path.exists() { return Ok(Vec::new()); @@ -193,11 +194,10 @@ fn read_cookies(user: &Cred) -> Result, Box> { fn save_cookies(user: &Cred, cookies: &[CookieVersion]) -> Result<(), Box> { debug!("Saving cookies: {cookies:?}"); - let path = Path::new(TS_LOCATION).join(user.user.uid.as_raw().to_string()); + let s_uid = user.user.uid.as_raw().to_string(); + let path = Path::new(TS_LOCATION).join(&s_uid); create_dir_all_with_privileges(path.parent().expect("Failed to get parent directory"))?; - let lockpath = Path::new(TS_LOCATION) - .join(user.user.uid.as_raw().to_string()) - .with_extension("lock"); + let lockpath = Path::new(TS_LOCATION).join(&s_uid).with_extension("lock"); let mut file = create_with_privileges(&path)?; cbor4ii::serde::to_writer(&mut file, &cookies)?; if let Err(err) = remove_with_privileges(lockpath) { @@ -216,7 +216,7 @@ fn find_valid_cookie( let mut res = None; debug!( "Constraints for {} : {:?}", - cred_asked.user.uid.as_raw(), + &cred_asked.user.uid.as_raw(), constraint ); for (a, cookiev) in cookies.iter_mut().enumerate() { @@ -333,6 +333,7 @@ mod test { fn test_cookie() { let cred = Cred { user: User::from_uid(0.into()).unwrap().unwrap(), + curdir: "".into(), groups: vec![], tty: None, ppid: Pid::parent(), diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml new file mode 100644 index 00000000..939d21f4 --- /dev/null +++ b/supply-chain/audits.toml @@ -0,0 +1,470 @@ + +# cargo-vet audits file + +[[audits.capctl]] +who = "LeChatP " +criteria = "safe-to-deploy" +version = "0.2.4" +notes = "The capctl crate demonstrates strong security practices with careful handling of Linux capabilities and syscalls." + +[[trusted.aho-corasick]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-03-28" +end = "2027-05-24" + +[[trusted.anstream]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-03-16" +end = "2027-05-24" + +[[trusted.anstyle]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2022-05-18" +end = "2027-05-24" + +[[trusted.anstyle-parse]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-03-08" +end = "2027-05-24" + +[[trusted.anstyle-query]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-04-13" +end = "2027-05-24" + +[[trusted.anstyle-wincon]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-03-08" +end = "2027-05-24" + +[[trusted.anyhow]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-10-05" +end = "2027-05-24" + +[[trusted.autocfg]] +criteria = "safe-to-deploy" +user-id = 539 # Josh Stone (cuviper) +start = "2019-05-22" +end = "2027-05-24" + +[[trusted.bon]] +criteria = "safe-to-deploy" +trusted-publisher = "github:elastio/bon" +start = "2025-07-18" +end = "2027-05-25" + +[[trusted.bon-macros]] +criteria = "safe-to-deploy" +trusted-publisher = "github:elastio/bon" +start = "2025-07-18" +end = "2027-05-25" + +[[trusted.bytes]] +criteria = "safe-to-deploy" +user-id = 6741 # Alice Ryhl (Darksonn) +start = "2021-01-11" +end = "2027-05-24" + +[[trusted.cc]] +criteria = "safe-to-deploy" +user-id = 55123 # rust-lang-owner +start = "2022-10-29" +end = "2027-05-24" + +[[trusted.cfg-if]] +criteria = "safe-to-deploy" +user-id = 55123 # rust-lang-owner +start = "2025-06-09" +end = "2027-05-24" + +[[trusted.clap]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2021-12-08" +end = "2027-05-24" + +[[trusted.clap_builder]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-03-28" +end = "2027-05-24" + +[[trusted.clap_derive]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2021-12-08" +end = "2027-05-24" + +[[trusted.clap_lex]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2022-04-15" +end = "2027-05-24" + +[[trusted.colorchoice]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-04-13" +end = "2027-05-24" + +[[trusted.env_filter]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2024-01-19" +end = "2027-05-24" + +[[trusted.env_logger]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2022-11-24" +end = "2027-05-24" + +[[trusted.find-msvc-tools]] +criteria = "safe-to-deploy" +user-id = 539 # Josh Stone (cuviper) +start = "2025-08-29" +end = "2027-05-24" + +[[trusted.glob]] +criteria = "safe-to-deploy" +user-id = 55123 # rust-lang-owner +start = "2023-01-06" +end = "2027-05-24" + +[[trusted.hashbrown]] +criteria = "safe-to-deploy" +user-id = 55123 # rust-lang-owner +start = "2025-04-30" +end = "2027-05-24" + +[[trusted.indexmap]] +criteria = "safe-to-deploy" +user-id = 539 # Josh Stone (cuviper) +start = "2020-01-15" +end = "2027-05-24" + +[[trusted.is_terminal_polyfill]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2024-05-02" +end = "2027-05-24" + +[[trusted.itoa]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-05-02" +end = "2027-05-24" + +[[trusted.jiff]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2024-02-17" +end = "2027-05-24" + +[[trusted.jiff-static]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2025-03-06" +end = "2027-05-24" + +[[trusted.jobserver]] +criteria = "safe-to-deploy" +user-id = 55123 # rust-lang-owner +start = "2024-07-23" +end = "2027-05-24" + +[[trusted.js-sys]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2027-05-24" + +[[trusted.libc]] +criteria = "safe-to-deploy" +user-id = 55123 # rust-lang-owner +start = "2024-08-15" +end = "2027-05-24" + +[[trusted.lock_api]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2019-05-04" +end = "2027-05-24" + +[[trusted.memchr]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-07-07" +end = "2027-05-24" + +[[trusted.nix]] +criteria = "safe-to-deploy" +user-id = 6094 # Alan Somers (asomers) +start = "2019-05-22" +end = "2027-05-25" + +[[trusted.once_cell_polyfill]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2025-05-22" +end = "2027-05-24" + +[[trusted.parking_lot]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2019-05-04" +end = "2027-05-24" + +[[trusted.parking_lot_core]] +criteria = "safe-to-deploy" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2019-05-04" +end = "2027-05-24" + +[[trusted.pcre2]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-04-14" +end = "2027-05-24" + +[[trusted.pcre2-sys]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-04-14" +end = "2027-05-24" + +[[trusted.prettyplease]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2022-01-04" +end = "2027-05-24" + +[[trusted.regex]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-02-27" +end = "2027-05-24" + +[[trusted.regex-automata]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-02-25" +end = "2027-05-24" + +[[trusted.regex-syntax]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-03-30" +end = "2027-05-24" + +[[trusted.rustversion]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-07-08" +end = "2027-05-24" + +[[trusted.scopeguard]] +criteria = "safe-to-run" +user-id = 2915 # Amanieu d'Antras (Amanieu) +start = "2020-02-16" +end = "2027-05-24" + +[[trusted.semver]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2021-05-25" +end = "2027-05-24" + +[[trusted.serde]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-03-01" +end = "2027-05-24" + +[[trusted.serde_core]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2025-09-13" +end = "2027-05-24" + +[[trusted.serde_derive]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-03-01" +end = "2027-05-24" + +[[trusted.serde_json]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-02-28" +end = "2027-05-24" + +[[trusted.serde_spanned]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-01-20" +end = "2027-05-24" + +[[trusted.serde_test]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-03-01" +end = "2027-05-24" + +[[trusted.slab]] +criteria = "safe-to-deploy" +user-id = 6741 # Alice Ryhl (Darksonn) +start = "2021-10-13" +end = "2027-05-24" + +[[trusted.syn]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-03-01" +end = "2027-05-24" + +[[trusted.thiserror]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-10-09" +end = "2027-05-24" + +[[trusted.thiserror-impl]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2019-10-09" +end = "2027-05-24" + +[[trusted.toml]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2022-12-14" +end = "2027-05-24" + +[[trusted.toml_parser]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2025-07-08" +end = "2027-05-24" + +[[trusted.toml_writer]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2025-07-08" +end = "2027-05-24" + +[[trusted.ucd-trie]] +criteria = "safe-to-deploy" +user-id = 189 # Andrew Gallant (BurntSushi) +start = "2019-07-21" +end = "2027-05-24" + +[[trusted.unicode-ident]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2021-10-02" +end = "2027-05-24" + +[[trusted.wasip2]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2025-09-09" +end = "2027-05-24" + +[[trusted.wasm-bindgen]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2027-05-24" + +[[trusted.wasm-bindgen-macro]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2027-05-25" + +[[trusted.wasm-bindgen-macro-support]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2027-05-24" + +[[trusted.wasm-bindgen-shared]] +criteria = "safe-to-deploy" +user-id = 1 # Alex Crichton (alexcrichton) +start = "2019-03-04" +end = "2027-05-24" + +[[trusted.winapi]] +criteria = "safe-to-deploy" +user-id = 63 # Peter Atashian (retep998) +start = "2019-04-03" +end = "2027-05-24" + +[[trusted.windows-core]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-15" +end = "2027-05-24" + +[[trusted.windows-implement]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-01-27" +end = "2027-05-24" + +[[trusted.windows-interface]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2022-02-18" +end = "2027-05-24" + +[[trusted.windows-link]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-07-17" +end = "2027-05-24" + +[[trusted.windows-result]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-02-02" +end = "2027-05-24" + +[[trusted.windows-strings]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2024-02-02" +end = "2027-05-24" + +[[trusted.windows-sys]] +criteria = "safe-to-deploy" +user-id = 64539 # Kenny Kerr (kennykerr) +start = "2021-11-15" +end = "2027-05-24" + +[[trusted.winnow]] +criteria = "safe-to-deploy" +user-id = 6743 # Ed Page (epage) +start = "2023-02-22" +end = "2027-05-24" + +[[trusted.wit-bindgen]] +criteria = "safe-to-deploy" +user-id = 73222 +start = "2024-02-23" +end = "2027-05-25" + +[[trusted.zmij]] +criteria = "safe-to-deploy" +user-id = 3618 # David Tolnay (dtolnay) +start = "2025-12-18" +end = "2027-05-24" diff --git a/supply-chain/config.toml b/supply-chain/config.toml new file mode 100644 index 00000000..1ddf78f5 --- /dev/null +++ b/supply-chain/config.toml @@ -0,0 +1,374 @@ + +# cargo-vet config file + +[cargo-vet] +version = "0.10" + +[imports.ariel-os] +url = "https://raw.githubusercontent.com/ariel-os/ariel-os/main/supply-chain/audits.toml" + +[imports.bytecode-alliance] +url = "https://raw.githubusercontent.com/bytecodealliance/wasmtime/main/supply-chain/audits.toml" + +[imports.google] +url = "https://raw.githubusercontent.com/google/supply-chain/main/audits.toml" + +[imports.isrg] +url = "https://raw.githubusercontent.com/divviup/libprio-rs/main/supply-chain/audits.toml" + +[imports.mozilla] +url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml" + +[policy.rootasrole] +audit-as-crates-io = true + +[policy.rootasrole-core] +audit-as-crates-io = true + +[[exemptions.block-buffer]] +version = "0.10.4" +criteria = "safe-to-deploy" + +[[exemptions.bytecheck]] +version = "0.8.2" +criteria = "safe-to-deploy" + +[[exemptions.bytecheck_derive]] +version = "0.8.2" +criteria = "safe-to-deploy" + +[[exemptions.cbor4ii]] +version = "1.2.2" +criteria = "safe-to-deploy" + +[[exemptions.cc]] +version = "1.2.62" +criteria = "safe-to-deploy" + +[[exemptions.cfg_aliases]] +version = "0.2.1" +criteria = "safe-to-deploy" + +[[exemptions.chrono]] +version = "0.4.44" +criteria = "safe-to-deploy" + +[[exemptions.const_format]] +version = "0.2.36" +criteria = "safe-to-deploy" + +[[exemptions.const_format_proc_macros]] +version = "0.2.34" +criteria = "safe-to-deploy" + +[[exemptions.const_panic]] +version = "0.2.15" +criteria = "safe-to-deploy" + +[[exemptions.cpufeatures]] +version = "0.2.17" +criteria = "safe-to-deploy" + +[[exemptions.crossbeam-utils]] +version = "0.8.21" +criteria = "safe-to-deploy" + +[[exemptions.crypto-common]] +version = "0.1.7" +criteria = "safe-to-deploy" + +[[exemptions.darling]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.darling_core]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.darling_macro]] +version = "0.23.0" +criteria = "safe-to-deploy" + +[[exemptions.derivative]] +version = "2.2.0" +criteria = "safe-to-deploy" + +[[exemptions.digest]] +version = "0.10.7" +criteria = "safe-to-deploy" + +[[exemptions.enumflags2]] +version = "0.7.12" +criteria = "safe-to-deploy" + +[[exemptions.enumflags2_derive]] +version = "0.7.12" +criteria = "safe-to-deploy" + +[[exemptions.error-chain]] +version = "0.12.4" +criteria = "safe-to-deploy" + +[[exemptions.find-msvc-tools]] +version = "0.1.9" +criteria = "safe-to-deploy" + +[[exemptions.futures-executor]] +version = "0.3.32" +criteria = "safe-to-run" + +[[exemptions.futures-task]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.futures-util]] +version = "0.3.32" +criteria = "safe-to-deploy" + +[[exemptions.generic-array]] +version = "0.14.7" +criteria = "safe-to-deploy" + +[[exemptions.getrandom]] +version = "0.3.4" +criteria = "safe-to-deploy" + +[[exemptions.hostname]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.iana-time-zone]] +version = "0.1.65" +criteria = "safe-to-deploy" + +[[exemptions.ident_case]] +version = "1.0.1" +criteria = "safe-to-deploy" + +[[exemptions.js-sys]] +version = "0.3.98" +criteria = "safe-to-deploy" + +[[exemptions.konst]] +version = "0.2.20" +criteria = "safe-to-deploy" + +[[exemptions.konst]] +version = "0.4.3" +criteria = "safe-to-deploy" + +[[exemptions.konst_macro_rules]] +version = "0.2.19" +criteria = "safe-to-deploy" + +[[exemptions.konst_proc_macros]] +version = "0.4.1" +criteria = "safe-to-deploy" + +[[exemptions.landlock]] +version = "0.4.4" +criteria = "safe-to-deploy" + +[[exemptions.libpam-sys]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.libpam-sys-helpers]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.libpam-sys-impls]] +version = "0.2.0" +criteria = "safe-to-deploy" + +[[exemptions.libseccomp]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.libseccomp-sys]] +version = "0.3.0" +criteria = "safe-to-deploy" + +[[exemptions.munge]] +version = "0.4.7" +criteria = "safe-to-deploy" + +[[exemptions.munge_macro]] +version = "0.4.7" +criteria = "safe-to-deploy" + +[[exemptions.nonstick]] +version = "0.1.2" +criteria = "safe-to-deploy" + +[[exemptions.num_threads]] +version = "0.1.7" +criteria = "safe-to-deploy" + +[[exemptions.once_cell]] +version = "1.21.4" +criteria = "safe-to-deploy" + +[[exemptions.pest]] +version = "2.8.6" +criteria = "safe-to-deploy" + +[[exemptions.pest_derive]] +version = "2.8.6" +criteria = "safe-to-deploy" + +[[exemptions.pest_generator]] +version = "2.8.6" +criteria = "safe-to-deploy" + +[[exemptions.pest_meta]] +version = "2.8.6" +criteria = "safe-to-deploy" + +[[exemptions.pin-project-lite]] +version = "0.2.17" +criteria = "safe-to-deploy" + +[[exemptions.pkg-config]] +version = "0.3.33" +criteria = "safe-to-deploy" + +[[exemptions.portable-atomic]] +version = "1.13.1" +criteria = "safe-to-deploy" + +[[exemptions.portable-atomic-util]] +version = "0.2.7" +criteria = "safe-to-deploy" + +[[exemptions.ptr_meta]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.ptr_meta_derive]] +version = "0.3.1" +criteria = "safe-to-deploy" + +[[exemptions.r-efi]] +version = "5.3.0" +criteria = "safe-to-deploy" + +[[exemptions.rancor]] +version = "0.1.1" +criteria = "safe-to-deploy" + +[[exemptions.redox_syscall]] +version = "0.5.18" +criteria = "safe-to-run" + +[[exemptions.rend]] +version = "0.5.3" +criteria = "safe-to-deploy" + +[[exemptions.ringbuf]] +version = "0.4.8" +criteria = "safe-to-deploy" + +[[exemptions.rkyv]] +version = "0.8.16" +criteria = "safe-to-deploy" + +[[exemptions.rkyv_derive]] +version = "0.8.16" +criteria = "safe-to-deploy" + +[[exemptions.rootasrole]] +version = "4.0.0" +criteria = "safe-to-deploy" + +[[exemptions.rootasrole-core]] +version = "4.0.0" +criteria = "safe-to-deploy" + +[[exemptions.scc]] +version = "2.4.0" +criteria = "safe-to-run" + +[[exemptions.sdd]] +version = "3.0.10" +criteria = "safe-to-run" + +[[exemptions.serial_test]] +version = "3.4.0" +criteria = "safe-to-run" + +[[exemptions.serial_test_derive]] +version = "3.4.0" +criteria = "safe-to-run" + +[[exemptions.shell-words]] +version = "1.1.1" +criteria = "safe-to-deploy" + +[[exemptions.simdutf8]] +version = "0.1.5" +criteria = "safe-to-deploy" + +[[exemptions.syslog]] +version = "6.1.1" +criteria = "safe-to-deploy" + +[[exemptions.test-log]] +version = "0.2.20" +criteria = "safe-to-run" + +[[exemptions.test-log-core]] +version = "0.2.20" +criteria = "safe-to-run" + +[[exemptions.test-log-macros]] +version = "0.2.20" +criteria = "safe-to-run" + +[[exemptions.time]] +version = "0.3.47" +criteria = "safe-to-deploy" + +[[exemptions.tinyvec]] +version = "1.11.0" +criteria = "safe-to-deploy" + +[[exemptions.typenum]] +version = "1.20.0" +criteria = "safe-to-deploy" + +[[exemptions.typewit]] +version = "1.15.2" +criteria = "safe-to-deploy" + +[[exemptions.uuid]] +version = "1.23.1" +criteria = "safe-to-deploy" + +[[exemptions.version_check]] +version = "0.9.5" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen]] +version = "0.2.121" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-macro]] +version = "0.2.121" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-macro-support]] +version = "0.2.121" +criteria = "safe-to-deploy" + +[[exemptions.wasm-bindgen-shared]] +version = "0.2.121" +criteria = "safe-to-deploy" + +[[exemptions.winapi-i686-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" + +[[exemptions.winapi-x86_64-pc-windows-gnu]] +version = "0.4.0" +criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock new file mode 100644 index 00000000..640236d8 --- /dev/null +++ b/supply-chain/imports.lock @@ -0,0 +1,1236 @@ + +# cargo-vet imports lock + +[[publisher.aho-corasick]] +version = "1.1.4" +when = "2025-10-28" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.anstream]] +version = "1.0.0" +when = "2026-02-11" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.anstyle]] +version = "1.0.14" +when = "2026-03-13" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.anstyle-parse]] +version = "1.0.0" +when = "2026-02-11" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.anstyle-query]] +version = "1.1.5" +when = "2025-11-13" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.anstyle-wincon]] +version = "3.0.11" +when = "2025-11-13" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.anyhow]] +version = "1.0.102" +when = "2026-02-20" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.autocfg]] +version = "1.5.0" +when = "2025-06-17" +user-id = 539 +user-login = "cuviper" +user-name = "Josh Stone" + +[[publisher.bon]] +version = "3.9.1" +when = "2026-03-13" +trusted-publisher = "github:elastio/bon" + +[[publisher.bon-macros]] +version = "3.9.1" +when = "2026-03-13" +trusted-publisher = "github:elastio/bon" + +[[publisher.bumpalo]] +version = "3.20.2" +when = "2026-02-19" +user-id = 696 +user-login = "fitzgen" +user-name = "Nick Fitzgerald" + +[[publisher.bytes]] +version = "1.11.1" +when = "2026-02-03" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.cfg-if]] +version = "1.0.4" +when = "2025-10-15" +user-id = 55123 +user-login = "rust-lang-owner" + +[[publisher.clap]] +version = "4.6.1" +when = "2026-04-15" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.clap_builder]] +version = "4.6.0" +when = "2026-03-12" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.clap_derive]] +version = "4.6.1" +when = "2026-04-15" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.clap_lex]] +version = "1.1.0" +when = "2026-03-12" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.colorchoice]] +version = "1.0.5" +when = "2026-03-13" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.env_filter]] +version = "1.0.1" +when = "2026-03-23" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.env_logger]] +version = "0.11.10" +when = "2026-03-23" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.glob]] +version = "0.3.3" +when = "2025-08-11" +user-id = 55123 +user-login = "rust-lang-owner" + +[[publisher.hashbrown]] +version = "0.17.1" +when = "2026-05-09" +user-id = 55123 +user-login = "rust-lang-owner" + +[[publisher.indexmap]] +version = "2.14.0" +when = "2026-04-09" +user-id = 539 +user-login = "cuviper" +user-name = "Josh Stone" + +[[publisher.is_terminal_polyfill]] +version = "1.70.2" +when = "2025-10-21" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.itoa]] +version = "1.0.18" +when = "2026-03-20" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.jiff]] +version = "0.2.24" +when = "2026-04-23" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.jiff-static]] +version = "0.2.24" +when = "2026-04-23" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.jobserver]] +version = "0.1.34" +when = "2025-08-23" +user-id = 55123 +user-login = "rust-lang-owner" + +[[publisher.libc]] +version = "0.2.186" +when = "2026-04-23" +user-id = 55123 +user-login = "rust-lang-owner" + +[[publisher.lock_api]] +version = "0.4.14" +when = "2025-10-03" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.memchr]] +version = "2.8.0" +when = "2026-02-06" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.nix]] +version = "0.27.1" +when = "2023-08-28" +user-id = 6094 +user-login = "asomers" +user-name = "Alan Somers" + +[[publisher.once_cell_polyfill]] +version = "1.70.2" +when = "2025-10-21" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.parking_lot]] +version = "0.12.5" +when = "2025-10-03" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.parking_lot_core]] +version = "0.9.12" +when = "2025-10-03" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.pcre2]] +version = "0.2.11" +when = "2025-09-25" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.pcre2-sys]] +version = "0.2.10" +when = "2025-09-21" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.prettyplease]] +version = "0.2.37" +when = "2025-08-19" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.regex]] +version = "1.12.3" +when = "2026-02-03" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex-automata]] +version = "0.4.14" +when = "2026-02-03" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.regex-syntax]] +version = "0.8.10" +when = "2026-02-24" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.rustversion]] +version = "1.0.22" +when = "2025-08-08" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.scopeguard]] +version = "1.2.0" +when = "2023-07-17" +user-id = 2915 +user-login = "Amanieu" +user-name = "Amanieu d'Antras" + +[[publisher.semver]] +version = "1.0.28" +when = "2026-04-04" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde]] +version = "1.0.228" +when = "2025-09-27" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_core]] +version = "1.0.228" +when = "2025-09-27" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_derive]] +version = "1.0.228" +when = "2025-09-27" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_json]] +version = "1.0.149" +when = "2026-01-06" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.serde_spanned]] +version = "1.1.1" +when = "2026-03-31" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.serde_test]] +version = "1.0.177" +when = "2024-08-05" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.slab]] +version = "0.4.12" +when = "2026-01-31" +user-id = 6741 +user-login = "Darksonn" +user-name = "Alice Ryhl" + +[[publisher.syn]] +version = "1.0.109" +when = "2023-02-24" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.syn]] +version = "2.0.117" +when = "2026-02-20" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.thiserror]] +version = "2.0.18" +when = "2026-01-18" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.thiserror-impl]] +version = "2.0.18" +when = "2026-01-18" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.toml]] +version = "0.9.12+spec-1.1.0" +when = "2026-02-10" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.toml_parser]] +version = "1.1.2+spec-1.1.0" +when = "2026-04-01" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.toml_writer]] +version = "1.1.1+spec-1.1.0" +when = "2026-03-31" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.ucd-trie]] +version = "0.1.7" +when = "2024-09-29" +user-id = 189 +user-login = "BurntSushi" +user-name = "Andrew Gallant" + +[[publisher.unicode-ident]] +version = "1.0.24" +when = "2026-02-16" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[publisher.unicode-xid]] +version = "0.2.6" +when = "2024-09-19" +user-id = 1139 +user-login = "Manishearth" +user-name = "Manish Goregaokar" + +[[publisher.wasip2]] +version = "1.0.3+wasi-0.2.9" +when = "2026-04-17" +user-id = 1 +user-login = "alexcrichton" +user-name = "Alex Crichton" + +[[publisher.winapi]] +version = "0.3.9" +when = "2020-06-26" +user-id = 63 +user-login = "retep998" +user-name = "Peter Atashian" + +[[publisher.windows-core]] +version = "0.62.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-implement]] +version = "0.60.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-interface]] +version = "0.59.3" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-link]] +version = "0.2.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-result]] +version = "0.4.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-strings]] +version = "0.5.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.windows-sys]] +version = "0.61.2" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + +[[publisher.winnow]] +version = "0.7.15" +when = "2026-03-05" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.winnow]] +version = "1.0.2" +when = "2026-04-21" +user-id = 6743 +user-login = "epage" +user-name = "Ed Page" + +[[publisher.wit-bindgen]] +version = "0.57.1" +when = "2026-04-17" +trusted-publisher = "github:bytecodealliance/wit-bindgen" + +[[publisher.zmij]] +version = "1.0.21" +when = "2026-02-12" +user-id = 3618 +user-login = "dtolnay" +user-name = "David Tolnay" + +[[audits.ariel-os.audits.futures-core]] +who = "Nils Ponsard " +criteria = "safe-to-deploy" +delta = "0.3.31 -> 0.3.32" + +[[audits.ariel-os.audits.num-conv]] +who = "Nils Ponsard " +criteria = "safe-to-deploy" +delta = "0.2.0 -> 0.2.1" + +[[audits.bytecode-alliance.wildcard-audits.bumpalo]] +who = "Nick Fitzgerald " +criteria = "safe-to-deploy" +user-id = 696 # Nick Fitzgerald (fitzgen) +start = "2019-03-16" +end = "2026-08-21" + +[[audits.bytecode-alliance.wildcard-audits.wit-bindgen]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +trusted-publisher = "github:bytecodealliance/wit-bindgen" +start = "2025-08-13" +end = "2027-01-08" +notes = "The Bytecode Alliance is the author of this crate" + +[[audits.bytecode-alliance.audits.bitflags]] +who = "Jamey Sharp " +criteria = "safe-to-deploy" +delta = "2.1.0 -> 2.2.1" +notes = """ +This version adds unsafe impls of traits from the bytemuck crate when built +with that library enabled, but I believe the impls satisfy the documented +safety requirements for bytemuck. The other changes are minor. +""" + +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "2.3.2 -> 2.3.3" +notes = """ +Nothing outside the realm of what one would expect from a bitflags generator, +all as expected. +""" + +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "2.4.1 -> 2.6.0" +notes = """ +Changes in how macros are invoked and various bits and pieces of macro-fu. +Otherwise no major changes and nothing dealing with `unsafe`. +""" + +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "2.7.0 -> 2.9.4" +notes = "Tweaks to the macro, nothing out of order." + +[[audits.bytecode-alliance.audits.bitflags]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "2.10.0 -> 2.11.1" +notes = "Minor updates, nothing awry here." + +[[audits.bytecode-alliance.audits.futures-core]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +version = "0.3.27" +notes = "Unsafe used to implement a concurrency primitive AtomicWaker. Well-commented and not obviously incorrect. Like my other audits of these concurrency primitives inside the futures family, I couldn't certify that it is correct without formal methods, but that is out of scope for this vetting." + +[[audits.bytecode-alliance.audits.futures-core]] +who = "Pat Hickey " +criteria = "safe-to-deploy" +delta = "0.3.28 -> 0.3.31" + +[[audits.bytecode-alliance.audits.heck]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +delta = "0.4.1 -> 0.5.0" +notes = "Minor changes for a `no_std` upgrade but otherwise everything looks as expected." + +[[audits.bytecode-alliance.audits.iana-time-zone-haiku]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +version = "0.1.2" + +[[audits.bytecode-alliance.audits.shlex]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "1.1.0" +notes = "Only minor `unsafe` code blocks which look valid and otherwise does what it says on the tin." + +[[audits.google.audits.bitflags]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.3.2" +notes = """ +Security review of earlier versions of the crate can be found at +(Google-internal, sorry): go/image-crate-chromium-security-review + +The crate exposes a function marked as `unsafe`, but doesn't use any +`unsafe` blocks (except for tests of the single `unsafe` function). I +think this justifies marking this crate as `ub-risk-1`. + +Additional review comments can be found at https://crrev.com/c/4723145/31 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.core-foundation-sys]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.8.7" +notes = "OSX system APIs" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.equivalent]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.equivalent]] +who = "Jonathan Hao " +criteria = "safe-to-deploy" +delta = "1.0.1 -> 1.0.2" +notes = "No changes to any .rs files or Rust code." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.heck]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "0.4.1" +notes = """ +Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'``, `'\bnet\b'``, `'\bunsafe\b'`` +and there were no hits. + +`heck` (version `0.3.3`) has been added to Chromium in +https://source.chromium.org/chromium/chromium/src/+/28841c33c77833cc30b286f9ae24c97e7a8f4057 +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.log]] +who = "danakj " +criteria = "safe-to-deploy" +version = "0.4.22" +notes = """ +Unsafe review in https://docs.google.com/document/d/1IXQbD1GhTRqNHIGxq6yy7qHqxeO4CwN5noMFXnqyDIM/edit?usp=sharing + +Unsafety is generally very well-documented, with one exception, which we +describe in the review doc. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.log]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "0.4.22 -> 0.4.25" +notes = "No impact on `unsafe` usage in `lib.rs`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.log]] +who = "Daniel Cheng " +criteria = "safe-to-deploy" +delta = "0.4.25 -> 0.4.26" +notes = "Only trivial code and documentation changes." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.match_cfg]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "0.1.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.num-traits]] +who = "Manish Goregaokar " +criteria = "safe-to-deploy" +version = "0.2.19" +notes = "Contains a single line of float-to-int unsafe with decent safety comments" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +version = "1.0.78" +notes = """ +Grepped for "crypt", "cipher", "fs", "net" - there were no hits +(except for a benign "fs" hit in a doc comment) + +Notes from the `unsafe` review can be found in https://crrev.com/c/5385745. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.78 -> 1.0.79" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Adrian Taylor " +criteria = "safe-to-deploy" +delta = "1.0.79 -> 1.0.80" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.80 -> 1.0.81" +notes = "Comment changes only" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.81 -> 1.0.82" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Dustin J. Mitchell " +criteria = "safe-to-deploy" +delta = "1.0.82 -> 1.0.83" +notes = "Substantive change is replacing String with Box, saving memory." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.83 -> 1.0.84" +notes = "Only doc comment changes in `src/lib.rs`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "danakj@chromium.org" +criteria = "safe-to-deploy" +delta = "1.0.84 -> 1.0.85" +notes = "Test-only changes." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Lukasz Anforowicz " +criteria = "safe-to-deploy" +delta = "1.0.85 -> 1.0.86" +notes = """ +Comment-only changes in `build.rs`. +Reordering of `Cargo.toml` entries. +Just bumping up the version number in `lib.rs`. +Config-related changes in `test_size.rs`. +""" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "danakj " +criteria = "safe-to-deploy" +delta = "1.0.86 -> 1.0.87" +notes = "No new unsafe interactions." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.proc-macro2]] +who = "Liza Burakova ", + "Erich Gubler ", +] +criteria = "safe-to-deploy" +delta = "2.6.0 -> 2.7.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.bitflags]] +who = "Benjamin VanderSloot " +criteria = "safe-to-deploy" +delta = "2.9.4 -> 2.10.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.deranged]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.3.11" +notes = """ +This crate contains a decent bit of `unsafe` code, however all internal +unsafety is verified with copious assertions (many are compile-time), and +otherwise the unsafety is documented and left to the caller to verify. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.deranged]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.3.11 -> 0.4.0" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.deranged]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.5.8" +notes = "New unsafe code is properly guarded" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.futures-core]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.3.27 -> 0.3.28" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.hex]] +who = "Simon Friedberger " +criteria = "safe-to-deploy" +version = "0.4.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.log]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "0.4.26 -> 0.4.29" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.27.1 -> 0.28.0" +notes = """ +Many new features and bugfixes. Obviously there's a lot of unsafe code calling +libc, but the usage looks correct. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.28.0 -> 0.29.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.nix]] +who = "Gabriele Svelto " +criteria = "safe-to-deploy" +delta = "0.29.0 -> 0.30.1" +notes = "Some new wrappers, support for minor platforms and lots of work around type safety that reduces the unsafe surafce." +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-conv]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.1.0" +notes = """ +Very straightforward, simple crate. No dependencies, unsafe, extern, +side-effectful std functions, etc. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.num-conv]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.2.0" +notes = "Revision only removes code" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.powerfmt]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.2.0" +notes = """ +A tiny bit of unsafe code to implement functionality that isn't in stable rust +yet, but it's all valid. Otherwise it's a pretty simple crate. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.proc-macro2]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.94 -> 1.0.106" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.quote]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +delta = "1.0.40 -> 1.0.45" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Jeff Muizelaar " +criteria = "safe-to-deploy" +delta = "0.10.6 -> 0.10.8" +notes = """ +The bulk of this is https://github.com/RustCrypto/hashes/pull/490 which adds aarch64 support along with another PR adding longson. +I didn't check the implementation thoroughly but there wasn't anything obviously nefarious. 0.10.8 has been out for more than a year +which suggests no one else has found anything either. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.shlex]] +who = "Max Inden " +criteria = "safe-to-deploy" +delta = "1.1.0 -> 1.3.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.smallvec]] +who = "Erich Gubler " +criteria = "safe-to-deploy" +delta = "1.14.0 -> 1.15.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strsim]] +who = "Ben Dean-Kawamura " +criteria = "safe-to-deploy" +delta = "0.10.0 -> 0.11.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.25.0 -> 0.26.3" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.strum_macros]] +who = "Teodor Tanasoaia " +criteria = "safe-to-deploy" +delta = "0.25.3 -> 0.26.4" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +version = "0.1.0" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.1.1 -> 0.1.2" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.1.2 -> 0.1.4" +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-core]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.1.4 -> 0.1.8" +notes = "No unsafe code" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +version = "0.2.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Kershaw Chang " +criteria = "safe-to-deploy" +delta = "0.2.6 -> 0.2.10" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +delta = "0.2.10 -> 0.2.18" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.2.18 -> 0.2.22" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.time-macros]] +who = "Lars Eggert " +criteria = "safe-to-deploy" +delta = "0.2.22 -> 0.2.27" +notes = "Refactors some unsafe code, nothing new" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.tinyvec_macros]] +who = "Drew Willcoxon " +criteria = "safe-to-deploy" +delta = "0.1.0 -> 0.1.1" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.toml_datetime]] +who = "Jan-Erik Rediger " +criteria = "safe-to-deploy" +version = "0.7.5+spec-1.1.0" +notes = "Pure data type crate with some datetime parsing. No unsafe." +aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" + +[[audits.mozilla.audits.utf8parse]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +delta = "0.2.1 -> 0.2.2" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" diff --git a/tests/helpers/config_manager.rs b/tests/helpers/config_manager.rs deleted file mode 100644 index 576b06d2..00000000 --- a/tests/helpers/config_manager.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::fs::{self, File}; -use std::io::Write; -use std::path::{Path, PathBuf}; - -use rar_common::database::versionning::Versioning; -use rar_common::{FullSettings, RemoteStorageSettings, SettingsContent, StorageMethod}; - -/// Manages the test configuration file that points to different policy fixtures -pub struct ConfigManager { - config_file_path: PathBuf, -} - -impl ConfigManager { - /// Creates a new ``ConfigManager`` instance - pub fn new(config_path: &Path) -> Result> { - let manager = Self { - config_file_path: config_path.to_path_buf(), - }; - - // Create initial empty configuration - manager.create_initial_config()?; - - Ok(manager) - } - - /// Creates the initial test configuration file - fn create_initial_config(&self) -> Result<(), Box> { - let settings = FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .build(), - ) - .build(); - - self.write_config(settings)?; - Ok(()) - } - - /// Load a specific policy fixture by updating the configuration - pub fn load_fixture(&self, fixture_path: &Path) -> Result<(), Box> { - // Create a configuration that points to the fixture file - let settings = FullSettings::builder() - .storage( - SettingsContent::builder() - .method(StorageMethod::JSON) - .settings( - RemoteStorageSettings::builder() - .path(fixture_path.canonicalize()?) - .not_immutable() - .build(), - ) - .build(), - ) - .build(); - - self.write_config(settings)?; - - Ok(()) - } - - /// Write configuration to the test config file - fn write_config(&self, settings: FullSettings) -> Result<(), Box> { - let mut file = File::create(&self.config_file_path).inspect_err(|e| { - eprintln!( - "Unable to create file {} : {}", - self.config_file_path.display(), - e - ); - })?; - let json = serde_json::to_string_pretty(&Versioning::new(settings)) - .inspect_err(|e| eprintln!("serializing error : {e}"))?; - file.write_all(json.as_bytes()) - .inspect_err(|e| eprintln!("unable to write config : {e}"))?; - file.flush() - .inspect_err(|e| eprintln!("Unable to flush data : {e}"))?; - Ok(()) - } -} - -impl Drop for ConfigManager { - fn drop(&mut self) { - // Clean up the configuration file - if self.config_file_path.exists() - && let Err(e) = fs::remove_file(&self.config_file_path) - { - eprintln!( - "Warning: Failed to clean up config file {}: {}", - self.config_file_path.display(), - e - ); - } - } -} diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs index c4ea1ace..80a70aa9 100644 --- a/tests/helpers/mod.rs +++ b/tests/helpers/mod.rs @@ -1,22 +1,22 @@ -pub mod config_manager; pub mod test_runner; use std::error::Error; use std::ffi::CString; use std::io::Write; +use std::os::unix::process::CommandExt; use std::os::unix::process::parent_id; use std::path::PathBuf; use std::process::Command; -use std::sync::{Mutex, Once, OnceLock}; +use std::sync::{Mutex, MutexGuard, Once, OnceLock}; use std::{env, fs}; -use nix::sys::wait::WaitStatus; -use nix::unistd::{User, fork, setgid, setgroups, setuid, unlink}; - -use crate::helpers::test_runner::TestRunner; +use nix::unistd::{User, setgid, setgroups, setuid, unlink}; +use rar_common::util::StorageMethod; const TEMP_LIFETIME_BUILD_STATE: &str = "target/tmp/dosr_integration_test_build"; const RAR_CFG_PATH: &str = "target/rootasrole.json"; +const RAR_CFG_DATA_PATH: &str = "target/rootasrole.json"; + static CLEANUP_REGISTERED: Once = Once::new(); fn register_cleanup() { @@ -51,7 +51,18 @@ fn cleanup_temp_files() { static GLOBAL_LOCK: OnceLock> = OnceLock::new(); -fn ensure_binary_built() -> Result> { +pub fn acquire_global_lock() -> MutexGuard<'static, ()> { + GLOBAL_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("Failed to acquire global lock") +} + +fn ensure_binary_built( + rar_cfg_path: &str, + rar_cfg_data_path: &str, + rar_cfg_type: StorageMethod, +) -> Result> { let pid = parent_id(); let temp_file = PathBuf::from(TEMP_LIFETIME_BUILD_STATE); @@ -67,31 +78,14 @@ fn ensure_binary_built() -> Result> { if needs_build && option_env!("SKIP_BUILD").is_none() { print!("Building dosr .... "); - - match unsafe { fork() } { - Ok(nix::unistd::ForkResult::Parent { child }) => { - // Parent process: wait for the child to finish - loop { - let wait_status = nix::sys::wait::waitpid(child, None) - .expect("Failed to wait for child process"); - if let WaitStatus::Exited(_, code) = wait_status { - if code != 0 { - return Err("Child process failed to build dosr binary".into()); - } // else - break; - } - } - } - Ok(nix::unistd::ForkResult::Child) => { - if let Err(e) = build_dosr_binary(pid, &temp_file) { - eprintln!("Error during build: {e}"); - std::process::exit(1); - } - std::process::exit(0); - } - Err(e) => { - return Err(format!("Fork failed: {e}").into()); - } + if let Err(e) = build_dosr_binary( + pid, + &temp_file, + rar_cfg_path, + rar_cfg_data_path, + rar_cfg_type, + ) { + return Err(format!("Build failed: {e}").into()); } } else { print!("Reusing binary ... "); @@ -101,7 +95,13 @@ fn ensure_binary_built() -> Result> { Ok("target/debug/dosr".into()) } -fn build_dosr_binary(pid: u32, temp_file: &PathBuf) -> Result<(), Box> { +fn build_dosr_binary( + pid: u32, + temp_file: &PathBuf, + rar_cfg_path: &str, + rar_cfg_data_path: &str, + rar_cfg_type: StorageMethod, +) -> Result<(), Box> { let user = User::from_name( &std::env::var("RAR_USER") .or_else(|_| std::env::var("SUDO_USER")) @@ -113,25 +113,31 @@ fn build_dosr_binary(pid: u32, temp_file: &PathBuf) -> Result<(), Box .inspect_err(|e| eprintln!("Failed to create CString: {e}"))?; let groups = nix::unistd::getgrouplist(user_name_cstr.as_c_str(), user.gid) .unwrap_or_else(|_| vec![user.gid]); - setgroups(&groups).inspect_err(|e| eprintln!("Failed to setgroups: {e}"))?; - setgid(user.gid).inspect_err(|e| eprintln!("Failed to setegid: {e}"))?; - setuid(user.uid).inspect_err(|e| eprintln!("Failed to seteuid: {e}"))?; - unsafe { - env::set_var( - "PATH", - format!("{}:{}/bin", env::var("PATH")?, env!("CARGO_HOME")), - ); - env::set_var("HOME", &user.dir); - } - - let cfg_path = PathBuf::from(RAR_CFG_PATH); - let output = Command::new("cargo") + let uid = user.uid; + let gid = user.gid; + let home_dir = user.dir; + let mut command = Command::new("cargo"); + command .args(["build", "--bin", "dosr", "--features", "finder"]) + .env("RAR_CFG_PATH", rar_cfg_path) + .env("RAR_CFG_DATA_PATH", rar_cfg_data_path) + .env("RAR_CFG_TYPE", rar_cfg_type.to_string()) + .env("RAR_AUTHENTICATION", "skip") .env( - "RAR_CFG_PATH", - cfg_path.to_str().ok_or("Invalid RAR_CFG_PATH")?, + "PATH", + format!("{}:{}/bin", env::var("PATH")?, env!("CARGO_HOME")), ) - .env("RAR_AUTHENTICATION", "skip") + .env("HOME", &home_dir); + unsafe { + command.pre_exec(move || { + let map_err = |e: nix::Error| std::io::Error::from_raw_os_error(e as i32); + setgroups(&groups).map_err(map_err)?; + setgid(gid).map_err(map_err)?; + setuid(uid).map_err(map_err)?; + Ok(()) + }); + } + let output = command .output() .inspect_err(|e| eprintln!("Failed to execute cargo build: {e}"))?; if !output.status.success() { @@ -142,15 +148,3 @@ fn build_dosr_binary(pid: u32, temp_file: &PathBuf) -> Result<(), Box print!("compiled binary ... "); Ok(()) } - -pub fn get_test_runner() -> Result> { - let _lock = GLOBAL_LOCK - .get_or_init(|| Mutex::new(())) - .lock() - .expect("Failed to acquire global lock"); - let binary_path = ensure_binary_built()?; - - register_cleanup(); - - TestRunner::new(binary_path, &PathBuf::from(RAR_CFG_PATH)) -} diff --git a/tests/helpers/test_runner.rs b/tests/helpers/test_runner.rs index d125d894..e5f0599a 100644 --- a/tests/helpers/test_runner.rs +++ b/tests/helpers/test_runner.rs @@ -1,12 +1,16 @@ use std::env; -use std::io::Result as IoResult; -use std::path::{Path, PathBuf}; +use std::io::{self, BufReader, Result as IoResult}; +use std::path::PathBuf; use std::process::{Command, Stdio}; use bon::bon; +use rar_common::database::versionning::Versioning; +use rar_common::file::{LockedSettingsFile, RootSettings}; +use rar_common::util::{RAR_CFG_TYPE, StorageMethod}; -use crate::helpers::config_manager::ConfigManager; - +use crate::helpers::{ + RAR_CFG_DATA_PATH, RAR_CFG_PATH, acquire_global_lock, ensure_binary_built, register_cleanup, +}; /// Represents the result of running the dosr command #[derive(Debug)] pub struct CommandResult { @@ -19,22 +23,59 @@ pub struct CommandResult { /// Main test runner that manages the dosr binary and test configurations pub struct TestRunner { binary_path: PathBuf, - config_manager: ConfigManager, + rar_cfg_path: String, + rar_cfg_type: StorageMethod, +} + +struct UserGroupGuard { + users: Vec, + groups: Vec, +} + +impl UserGroupGuard { + const fn new() -> Self { + Self { + users: Vec::new(), + groups: Vec::new(), + } + } + fn add_user(&mut self, u: String) { + self.users.push(u); + } + fn add_group(&mut self, g: String) { + self.groups.push(g); + } +} + +impl Drop for UserGroupGuard { + fn drop(&mut self) { + for user in &self.users { + let _ = Command::new("userdel").args(["-r", user]).status(); + } + for group in &self.groups { + let _ = Command::new("groupdel").args([group]).status(); + } + } } #[bon] #[allow(clippy::unwrap_used)] impl TestRunner { /// Creates a new ``TestRunner`` instance and compiles the dosr binary + #[builder] pub fn new( - binary_path: PathBuf, - test_config_path: &Path, + #[builder(default = RAR_CFG_PATH)] rar_cfg_path: &str, + #[builder(default = RAR_CFG_DATA_PATH)] rar_cfg_data_path: &str, + #[builder(default = RAR_CFG_TYPE)] rar_cfg_type: StorageMethod, ) -> Result> { - let config_manager = ConfigManager::new(test_config_path)?; + let _lock = acquire_global_lock(); + let binary_path = ensure_binary_built(rar_cfg_path, rar_cfg_data_path, rar_cfg_type)?; + register_cleanup(); Ok(Self { binary_path, - config_manager, + rar_cfg_path: rar_cfg_path.to_string(), + rar_cfg_type, }) } @@ -43,19 +84,36 @@ impl TestRunner { pub fn run_dosr( &self, #[builder(start_fn)] args: &[&str], - fixture_name: Option<&str>, + rar_cfg_data_path: Option<&str>, env_vars: Option<&[(&str, &str)]>, users: Option<&[&str]>, groups: Option<&[&str]>, ) -> IoResult { - // If a fixture is specified, update the configuration - if let Some(fixture) = fixture_name - && let Err(e) = self.config_manager.load_fixture(Path::new(fixture)) - { - eprintln!("Warning: Failed to load fixture '{fixture}': {e}"); + let _lock = acquire_global_lock(); + + if let Some(data_path) = rar_cfg_data_path { + let mut settings_file: LockedSettingsFile> = + LockedSettingsFile::open_write(self.rar_cfg_path.clone(), |_, file| { + let settings: Versioning = match self.rar_cfg_type { + StorageMethod::JSON => serde_json::from_reader(file)?, + StorageMethod::CBOR => cbor4ii::serde::from_reader(BufReader::new(file)) + .map_err(io::Error::other)?, + }; + Ok(settings) + })?; + settings_file + .data + .data + .storage + .settings + .get_or_insert_default() + .path = Some(data_path.into()); + settings_file + .save(self.rar_cfg_type, false) + .map_err(|e| io::Error::other(e.to_string()))?; } - let mut added_users = Vec::new(); - let mut added_groups = Vec::new(); + + let mut guard = UserGroupGuard::new(); if let Some(user_list) = users { // Check if users exist and create them if necessary for &user in user_list { @@ -70,7 +128,7 @@ impl TestRunner { println!("Warning: Failed to create user '{user}': {e}"); } println!("Created user '{user}' for testing purposes"); - added_users.push(user.to_string()); + guard.add_user(user.to_string()); } println!("User '{user}' exists"); } @@ -92,7 +150,7 @@ impl TestRunner { if let Err(e) = create_status { println!("Warning: Failed to create group '{group}': {e}"); } - added_groups.push(group.to_string()); + guard.add_group(group.to_string()); println!("Created group '{group}' for testing purposes"); } } @@ -102,9 +160,7 @@ impl TestRunner { } } } - let mut command = Command::new(&self.binary_path); - println!("Running command: {command:?} {args:?}"); - command + let output = Command::new(&self.binary_path) .args(args) .envs( env::vars().chain( @@ -115,9 +171,8 @@ impl TestRunner { ), ) .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - let output = command.output()?; + .stderr(Stdio::piped()) + .output()?; println!( "Output : {}", String::from_utf8(output.stdout.clone()).unwrap() @@ -127,14 +182,6 @@ impl TestRunner { String::from_utf8(output.stderr.clone()).unwrap() ); - // Clean up any users or groups we added - for user in added_users { - let _ = Command::new("userdel").args(["-r", &user]).status()?; - } - for group in added_groups { - let _ = Command::new("groupdel").args([&group]).status()?; - } - Ok(CommandResult { success: output.status.success(), exit_code: output.status.code().unwrap_or(-1), diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 0ebe414e..2d43b7da 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -5,12 +5,14 @@ mod tests { use pcre2::bytes::RegexBuilder; use serial_test::serial; - use crate::helpers::get_test_runner; + use crate::helpers::test_runner::TestRunner; #[test] #[serial] fn test_dosr_help() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["--help"]) .call() @@ -24,7 +26,9 @@ mod tests { #[test] #[serial] fn test_dosr_version() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["--version"]) .call() @@ -42,10 +46,12 @@ mod tests { #[test] #[serial] fn test_dosr_role_selection() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["--role", "B", "env"]) - .fixture_name("tests/fixtures/multi_role.json") + .rar_cfg_data_path("tests/fixtures/multi_role.json") .call() .expect("Failed to run dosr with invalid role"); assert!( @@ -61,10 +67,12 @@ mod tests { #[test] #[serial] fn test_dosr_task_selection() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["--role", "A", "--task", "A_B", "env"]) - .fixture_name("tests/fixtures/multi_role.json") + .rar_cfg_data_path("tests/fixtures/multi_role.json") .call() .expect("Failed to run dosr with invalid task"); assert!( @@ -80,10 +88,12 @@ mod tests { #[test] #[serial] fn test_dosr_invalid_role() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["--role", "C", "env"]) - .fixture_name("tests/fixtures/multi_role.json") + .rar_cfg_data_path("tests/fixtures/multi_role.json") .call() .expect("Failed to run dosr with invalid role"); assert!(!result.success, "Command unexpectedly succeeded"); @@ -96,10 +106,12 @@ mod tests { #[test] #[serial] fn test_dosr_env_override() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["-E", "--role", "env", "--task", "allowed", "env"]) - .fixture_name("tests/fixtures/env_override.json") + .rar_cfg_data_path("tests/fixtures/env_override.json") .env_vars(&[ ("KEEP", ""), ("TZ", "Europe/Paris"), @@ -123,10 +135,12 @@ mod tests { #[test] #[serial] fn test_dosr_env_override_not_overriden() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["--role", "env", "--task", "allowed", "env"]) - .fixture_name("tests/fixtures/env_override.json") + .rar_cfg_data_path("tests/fixtures/env_override.json") .env_vars(&[ ("KEEP", ""), ("TZ", "Europe/Paris"), @@ -150,10 +164,12 @@ mod tests { #[test] #[serial] fn test_dosr_env_override_denied() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["-E", "--role", "env", "--task", "denied", "env"]) - .fixture_name("tests/fixtures/env_override.json") + .rar_cfg_data_path("tests/fixtures/env_override.json") .env_vars(&[ ("KEEP", ""), ("TZ", "Europe/Paris"), @@ -174,10 +190,15 @@ mod tests { #[test] #[serial] fn test_dosr_env_override_denied_not_overriden() { - let runner = get_test_runner().expect("Failed to setup test environment"); + env_logger::builder() + .filter_level(log::LevelFilter::Trace) + .build(); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["--role", "env", "--task", "denied", "env"]) - .fixture_name("tests/fixtures/env_override.json") + .rar_cfg_data_path("tests/fixtures/env_override.json") .env_vars(&[ ("KEEP", ""), ("TZ", "Europe/Paris"), @@ -201,10 +222,12 @@ mod tests { #[test] #[serial] fn test_dosr_as_user() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["-u", "nobody", "id"]) - .fixture_name("tests/fixtures/user_group.json") + .rar_cfg_data_path("tests/fixtures/user_group.json") .users(&["nobody"]) .call() .expect("Failed to run dosr -u nobody id"); @@ -217,10 +240,12 @@ mod tests { #[test] #[serial] fn test_dosr_as_group() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["-g", "nobody", "id"]) - .fixture_name("tests/fixtures/user_group.json") + .rar_cfg_data_path("tests/fixtures/user_group.json") .users(&["nobody"]) .groups(&["nobody"]) .call() @@ -238,14 +263,15 @@ mod tests { #[test] #[serial] fn test_dosr_as_user_and_group() { - let runner = get_test_runner() + let runner = TestRunner::builder() + .build() .inspect_err(|e| eprintln!("Failed to setup test environment: {e}")) .unwrap(); let result = runner .run_dosr(&["-u", "nobody", "-g", "daemon,nobody", "id"]) .users(&["nobody"]) .groups(&["nobody", "daemon"]) - .fixture_name("tests/fixtures/user_group.json") + .rar_cfg_data_path("tests/fixtures/user_group.json") .env_vars(&[("LANG", "en_US")]) .call() .inspect_err(|e| eprintln!("Failed to run dosr -u nobody -g daemon,nobody id: {e}")) @@ -281,10 +307,12 @@ mod tests { println!("Skipping test_dosr_auth because RAR_PAM_SERVICE is set to original dosr"); return; } - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["/usr/bin/true"]) - .fixture_name("tests/fixtures/perform_auth.json") + .rar_cfg_data_path("tests/fixtures/perform_auth.json") .call() .expect("Failed to run dosr with auth role"); assert!( @@ -299,7 +327,7 @@ mod tests { // run dosr -K to delete the timestamp cookie let result = runner .run_dosr(&["-K"]) - .fixture_name("tests/fixtures/perform_auth.json") + .rar_cfg_data_path("tests/fixtures/perform_auth.json") .call() .expect("Failed to run dosr with auth role"); assert!( @@ -314,10 +342,12 @@ mod tests { #[test] #[serial] fn test_dosr_info() { - let runner = get_test_runner().expect("Failed to setup test environment"); + let runner = TestRunner::builder() + .build() + .expect("Failed to setup test environment"); let result = runner .run_dosr(&["--info", "-r", "A", "cat", "/proc/self/status"]) - .fixture_name("tests/fixtures/multi_role.json") + .rar_cfg_data_path("tests/fixtures/multi_role.json") .call() .expect("Failed to run dosr --info"); assert!(result.success, "Command failed: {}", result.stderr); diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 4295432d..f003028a 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -6,6 +6,13 @@ edition = "2024" publish = false +[lints.clippy] +pedantic = { level = "warn", priority = 1 } +nursery = { level = "warn", priority = 1 } +unwrap_used = { level = "deny", priority = 2 } +similar_names = { level = "allow", priority = 2 } +should_implement_trait = { level = "allow", priority = 2 } + [dependencies] anyhow = "1.0.86" clap = { version = "4.5.16", features = ["derive"] } diff --git a/xtask/src/configure.rs b/xtask/src/configure.rs index a93b83c3..8e735c74 100644 --- a/xtask/src/configure.rs +++ b/xtask/src/configure.rs @@ -78,30 +78,46 @@ pub fn check_filesystem() -> io::Result<()> { Ok(()) } +#[allow(clippy::too_many_lines)] fn set_options(content: &mut String) -> io::Result<()> { let mut config: SettingsFile = serde_json::from_str(content)?; - config.storage.method = env!("RAR_CFG_TYPE").parse().unwrap(); + config.storage.method = env!("RAR_CFG_TYPE") + .parse() + .expect("Check RAR_CFG_TYPE in .cargo/config.toml"); if let Some(settings) = &mut config.storage.settings { if let Some(path) = &mut settings.path { *path = env!("RAR_CFG_DATA_PATH").to_string(); } if let Some(immutable) = &mut settings.immutable { - *immutable = env!("RAR_CFG_IMMUTABLE").parse().unwrap(); + *immutable = env!("RAR_CFG_IMMUTABLE") + .parse() + .expect("Check RAR_CFG_IMMUTABLE in .cargo/config.toml"); } } config.storage.options = Some(Opt { timeout: Some(STimeout { - type_field: Some(env!("RAR_TIMEOUT_TYPE").parse().unwrap()), - duration: convert_string_to_duration(env!("RAR_TIMEOUT_DURATION")).unwrap(), + type_field: Some( + env!("RAR_TIMEOUT_TYPE") + .parse() + .expect("Check RAR_TIMEOUT_TYPE in .cargo/config.toml"), + ), + duration: convert_string_to_duration(env!("RAR_TIMEOUT_DURATION")) + .expect("Check RAR_TIMEOUT_DURATION in .cargo/config.toml"), max_usage: if env!("RAR_TIMEOUT_MAX_USAGE").is_empty() { None } else { - Some(env!("RAR_TIMEOUT_MAX_USAGE").parse().unwrap()) + Some( + env!("RAR_TIMEOUT_MAX_USAGE") + .parse() + .expect("Check RAR_TIMEOUT_MAX_USAGE in .cargo/config.toml"), + ) }, extra_fields: Value::Null, }), path: Some(SPathOptions { - default_behavior: env!("RAR_PATH_DEFAULT").parse().unwrap(), + default_behavior: env!("RAR_PATH_DEFAULT") + .parse() + .expect("Check RAR_PATH_DEFAULT in .cargo/config.toml"), add: Some( env!("RAR_PATH_ADD_LIST") .split(':') @@ -121,9 +137,18 @@ fn set_options(content: &mut String) -> io::Result<()> { extra_fields: Value::Null, }), env: Some(SEnvOptions { - default_behavior: env!("RAR_ENV_DEFAULT").parse().unwrap(), - override_behavior: if env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().unwrap() { - Some(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().unwrap()) + default_behavior: env!("RAR_ENV_DEFAULT") + .parse() + .expect("Check RAR_ENV_DEFAULT in .cargo/config.toml"), + override_behavior: if env!("RAR_ENV_OVERRIDE_BEHAVIOR") + .parse() + .expect("Check RAR_ENV_OVERRIDE_BEHAVIOR in .cargo/config.toml") + { + Some( + env!("RAR_ENV_OVERRIDE_BEHAVIOR") + .parse() + .expect("Check RAR_ENV_OVERRIDE_BEHAVIOR in .cargo/config.toml"), + ) } else { None }, @@ -148,13 +173,26 @@ fn set_options(content: &mut String) -> io::Result<()> { set: if env!("RAR_ENV_SET_LIST").is_empty() { HashMap::new() } else { - serde_json::from_str(env!("RAR_ENV_SET_LIST")).unwrap() + serde_json::from_str(env!("RAR_ENV_SET_LIST")) + .expect("Check RAR_ENV_SET_LIST in .cargo/config.toml") }, extra_fields: Value::Null, }), - root: Some(env!("RAR_USER_CONSIDERED").parse().unwrap()), - bounding: Some(env!("RAR_BOUNDING").parse().unwrap()), - authentication: Some(env!("RAR_AUTHENTICATION").parse().unwrap()), + root: Some( + env!("RAR_USER_CONSIDERED") + .parse() + .expect("Check RAR_USER_CONSIDERED in .cargo/config.toml"), + ), + bounding: Some( + env!("RAR_BOUNDING") + .parse() + .expect("Check RAR_BOUNDING in .cargo/config.toml"), + ), + authentication: Some( + env!("RAR_AUTHENTICATION") + .parse() + .expect("Check RAR_AUTHENTICATION in .cargo/config.toml"), + ), extra_fields: Value::Null, }); *content = serde_json::to_string_pretty(&config)?; @@ -172,36 +210,49 @@ fn set_immutable(config: &mut SettingsFile, value: bool) { let roles = config .extra_fields .as_object_mut() - .unwrap() + .expect("Config extra fields should be a JSON object") .get_mut("roles") - .unwrap() + .expect("Config should have roles field") .as_array_mut() - .unwrap(); + .expect("Roles field should be an array"); for role in roles { - let tasks = role.as_object_mut().unwrap().get_mut("tasks"); + let tasks = role + .as_object_mut() + .expect("Role should be a JSON object") + .get_mut("tasks"); if let Some(tasks) = tasks { - for task in tasks.as_array_mut().unwrap() { + for task in tasks + .as_array_mut() + .expect("Tasks field should be an array") + { let cred = task .as_object_mut() - .unwrap() + .expect("Task shoudl be a JSON object") .get_mut("cred") - .unwrap() + .expect("Task should have cred field") .as_object_mut() - .unwrap(); + .expect("Cred field should be a JSON object"); let caps = cred .get_mut("capabilities") - .unwrap() - .as_object_mut() - .unwrap(); - if let Some(add) = caps.get_mut("add") { - add.as_array_mut() - .unwrap() - .retain(|x| x.as_str().unwrap() != "CAP_LINUX_IMMUTABLE"); - } - if let Some(sub) = caps.get_mut("sub") { - sub.as_array_mut() - .unwrap() - .retain(|x| x.as_str().unwrap() != "CAP_LINUX_IMMUTABLE"); + .expect("Cred should have capabilities field"); + + if let Some(caps_obj) = caps.as_object_mut() { + if let Some(add) = caps_obj.get_mut("add") { + add.as_array_mut() + .expect("Add field should be an array") + .retain(|x| x != "CAP_LINUX_IMMUTABLE"); + } + if let Some(sub) = caps_obj.get_mut("sub") { + sub.as_array_mut() + .expect("Sub field should be an array") + .retain(|x| x != "CAP_LINUX_IMMUTABLE"); + } + } else if let Some(caps_arr) = caps.as_array_mut() { + caps_arr.retain(|x| x != "CAP_LINUX_IMMUTABLE"); + } else { + warn!( + "Unsupported capabilities format in config, expected object or array" + ); } } } @@ -217,18 +268,15 @@ fn get_filesystem_type>(path: P) -> io::Result> { let mut filesystem_type = None; for line_result in reader.lines() { - if let Ok(line_result) = line_result { - let fields: Vec<&str> = line_result.split_whitespace().collect(); - if fields.len() > 2 { - let mount_point = fields[1]; - let fs_type = fields[2]; - if path.starts_with(mount_point) && mount_point.len() > longest_mount_point.len() { - longest_mount_point = mount_point.to_string(); - filesystem_type = Some(fs_type.to_string()); - } + let line = line_result?; + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() > 2 { + let mount_point = fields[1]; + let fs_type = fields[2]; + if path.starts_with(mount_point) && mount_point.len() > longest_mount_point.len() { + longest_mount_point = mount_point.to_string(); + filesystem_type = Some(fs_type.to_string()); } - } else { - return Err(line_result.unwrap_err()); } } @@ -315,13 +363,16 @@ fn retrieve_real_user() -> Result, anyhow::Error> { } } -pub const fn pam_config(os: &OsTarget) -> &'static str { +pub fn pam_config(os: &OsTarget) -> std::io::Result { match os { OsTarget::Debian | OsTarget::Ubuntu => { - include_str!("../../resources/debian/deb_sr_pam.conf") + std::fs::read_to_string("../../resources/debian/deb_sr_pam.conf") + } + OsTarget::RedHat | OsTarget::Fedora => { + std::fs::read_to_string("../../resources/rh/rh_sr_pam.conf") } - OsTarget::RedHat | OsTarget::Fedora => include_str!("../../resources/rh/rh_sr_pam.conf"), - OsTarget::ArchLinux => include_str!("../../resources/arch/arch_sr_pam.conf"), + OsTarget::OpenSUSE => std::fs::read_to_string("../../resources/opensuse/opensuse.conf"), + OsTarget::ArchLinux => std::fs::read_to_string("../../resources/arch/arch_sr_pam.conf"), } } @@ -329,7 +380,7 @@ fn deploy_pam_config(os: &OsTarget) -> io::Result { if fs::metadata(Path::new("/etc/pam.d").join(PAM_CONFIG_SERVICE)).is_err() { info!("Deploying PAM configuration file"); let mut pam_conf = File::create(Path::new("/etc/pam.d").join(PAM_CONFIG_SERVICE))?; - pam_conf.write_all(pam_config(os).as_bytes())?; + pam_conf.write_all(pam_config(os)?.as_bytes())?; pam_conf.sync_all()?; } Ok(0) diff --git a/xtask/src/deploy/debian.rs b/xtask/src/deploy/debian.rs index 372f7101..1de06482 100644 --- a/xtask/src/deploy/debian.rs +++ b/xtask/src/deploy/debian.rs @@ -1,27 +1,28 @@ use std::{ fs::File, io::{BufRead, Write}, - process::{Command, ExitStatus}, + path::Path, + process::Command, }; -use anyhow::Context; +use anyhow::{Context, anyhow}; use log::debug; use crate::{ installer::{self, InstallDependenciesOptions, Profile, dependencies::install_dependencies}, - util::{OsTarget, detect_priv_bin, get_os}, + util::{OsTarget, detect_priv_bin, get_os, output_checked, run_checked}, }; use super::setup_maint_scripts; -fn dependencies(os: &OsTarget, priv_bin: Option<&String>) -> Result { +fn dependencies(os: &OsTarget, priv_bin: Option<&Path>) -> Result<(), anyhow::Error> { 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") + run_checked( + Command::new("cargo").arg("install").arg("cargo-deb"), + "install cargo-deb", + ) + .context("failed to install cargo-deb") } fn generate_changelog() -> Result<(), anyhow::Error> { @@ -29,27 +30,30 @@ fn generate_changelog() -> Result<(), anyhow::Error> { if std::path::Path::new(changelog_path).exists() { return Ok(()); } - let binding = Command::new("git") - .args(["tag", "--sort=-creatordate"]) - .output()?; + let binding = output_checked( + Command::new("git").args(["tag", "--sort=-creatordate"]), + "list git tags", + )?; let mut ordered_tags = binding.stdout.lines(); let from = ordered_tags .next() - .expect("Are you in the git repository ?")?; + .ok_or_else(|| anyhow!("No git tag found for changelog generation"))??; let to = ordered_tags .next() - .expect("Are you in the git repository ?")?; + .ok_or_else(|| anyhow!("At least two git tags are required for changelog generation"))??; debug!("Generating changelog from {from} to {to}"); - let changes = Command::new("git") - .args(["log", "--pretty=format: %s", &format!("{to}..{from}")]) - .output()?; + let changes = output_checked( + Command::new("git").args(["log", "--pretty=format: %s", &format!("{to}..{from}")]), + "collect changelog entries", + )?; debug!( "Changes: {}", - String::from_utf8(changes.stdout.clone()).unwrap() + String::from_utf8(changes.stdout.clone()) + .expect("Failed to convert git log output to string") ); let changelog = format!( r"rootasrole ({version}) {dist}; urgency={urgency} @@ -60,7 +64,8 @@ fn generate_changelog() -> Result<(), anyhow::Error> { version = env!("CARGO_PKG_VERSION"), dist = "unstable", urgency = "low", - changes = String::from_utf8(changes.stdout).unwrap(), + changes = + String::from_utf8(changes.stdout).expect("Failed to convert git log output to string"), date = chrono::Local::now().format("%a, %d %b %Y %T %z") ); File::create(changelog_path)?.write_all(changelog.as_bytes())?; @@ -71,11 +76,11 @@ fn generate_changelog() -> Result<(), anyhow::Error> { pub fn make_deb( os: Option<&OsTarget>, profile: Profile, - priv_bin: Option<&String>, + priv_bin: Option<&Path>, ) -> Result<(), anyhow::Error> { let os = get_os(os)?; - let priv_bin = priv_bin.cloned().or_else(detect_priv_bin); - dependencies(&os, priv_bin.as_ref())?; + let priv_bin = priv_bin.map(Path::to_path_buf).or_else(detect_priv_bin); + dependencies(&os, priv_bin.as_deref())?; installer::dependencies(&InstallDependenciesOptions { os: Some(os), @@ -87,14 +92,14 @@ pub fn make_deb( profile, toolchain: installer::Toolchain::default(), clean_before: false, - privbin: priv_bin, + priv_bin, })?; setup_maint_scripts()?; generate_changelog()?; - Command::new("cargo") - .arg("deb") - .arg("--no-build") - .status()?; + run_checked( + Command::new("cargo").arg("deb").arg("--no-build"), + "generate deb package", + )?; Ok(()) } diff --git a/xtask/src/deploy/mod.rs b/xtask/src/deploy/mod.rs index 7cb00eef..d1fe547b 100644 --- a/xtask/src/deploy/mod.rs +++ b/xtask/src/deploy/mod.rs @@ -1,7 +1,8 @@ -use std::{collections::HashSet, process::Command}; +use std::{collections::HashSet, path::PathBuf, process::Command}; use clap::Parser; +use crate::util::{is_dry_run, run_checked}; use crate::{installer::Profile, util::OsTarget}; mod debian; @@ -21,8 +22,8 @@ pub struct MakeOptions { pub target: Vec, /// The binary to elevate privileges - #[clap(long, short = 'p')] - pub priv_bin: Option, + #[clap(long, short = 'p', visible_alias = "privbin")] + pub priv_bin: Option, } fn all() -> HashSet { @@ -32,6 +33,11 @@ fn all() -> HashSet { } pub fn deploy(opts: &MakeOptions) -> Result<(), anyhow::Error> { + if is_dry_run() { + log::debug!("Dry-run mode: skipping deploy changes"); + return Ok(()); + } + let targets = if opts.target.is_empty() { all() } else { @@ -41,10 +47,10 @@ pub fn deploy(opts: &MakeOptions) -> Result<(), anyhow::Error> { for target in targets { match target { OsTarget::Debian => { - debian::make_deb(opts.os.as_ref(), opts.profile, opts.priv_bin.as_ref())?; + debian::make_deb(opts.os.as_ref(), opts.profile, opts.priv_bin.as_deref())?; } OsTarget::RedHat => { - redhat::make_rpm(opts.os.as_ref(), opts.profile, opts.priv_bin.as_ref())?; + redhat::make_rpm(opts.os.as_ref(), opts.profile, opts.priv_bin.as_deref())?; } _ => anyhow::bail!("Unsupported OS target"), } @@ -54,26 +60,27 @@ pub fn deploy(opts: &MakeOptions) -> Result<(), anyhow::Error> { } pub fn setup_maint_scripts() -> Result<(), anyhow::Error> { - Command::new("cargo") - .arg("build") - .arg("--package") - .arg("xtask") - .arg("--no-default-features") - .arg("--release") - .arg("--bin") - .arg("postinst") - .arg("--bin") - .arg("prerm") - .status()?; + run_checked( + Command::new("cargo") + .arg("build") + .arg("--package") + .arg("xtask") + .arg("--no-default-features") + .arg("--release") + .arg("--bin") + .arg("postinst") + .arg("--bin") + .arg("prerm"), + "build maintenance scripts", + )?; compress("target/release/postinst")?; compress("target/release/prerm") } fn compress(script: &str) -> Result<(), anyhow::Error> { - Command::new("upx") - .arg("--best") - .arg("--lzma") - .arg(script) - .status()?; + run_checked( + Command::new("upx").arg("--best").arg("--lzma").arg(script), + &format!("compress script {script}"), + )?; Ok(()) } diff --git a/xtask/src/deploy/redhat.rs b/xtask/src/deploy/redhat.rs index 8cebc5ab..c472fef9 100644 --- a/xtask/src/deploy/redhat.rs +++ b/xtask/src/deploy/redhat.rs @@ -1,26 +1,29 @@ +use std::path::Path; use std::process::Command; use crate::{ installer::{self, InstallDependenciesOptions, Profile}, - util::{OsTarget, detect_priv_bin, get_os}, + util::{OsTarget, detect_priv_bin, get_os, run_checked}, }; fn install_dependencies() -> Result<(), anyhow::Error> { - Command::new("cargo") - .arg("install") - .arg("cargo-generate-rpm") - .status()?; + run_checked( + Command::new("cargo") + .arg("install") + .arg("cargo-generate-rpm"), + "install cargo-generate-rpm", + )?; Ok(()) } pub fn make_rpm( os: Option<&OsTarget>, profile: Profile, - exe: Option<&String>, + exe: Option<&Path>, ) -> Result<(), anyhow::Error> { install_dependencies()?; let os = get_os(os)?; - let exe: Option = exe.cloned().or_else(detect_priv_bin); + let exe = exe.map(Path::to_path_buf).or_else(detect_priv_bin); installer::dependencies(&InstallDependenciesOptions { os: Some(os), @@ -32,9 +35,12 @@ pub fn make_rpm( profile, toolchain: installer::Toolchain::default(), clean_before: false, - privbin: exe, + priv_bin: exe, })?; - Command::new("cargo").arg("generate-rpm").status()?; + run_checked( + Command::new("cargo").arg("generate-rpm"), + "generate rpm package", + )?; Ok(()) } diff --git a/xtask/src/doctor.rs b/xtask/src/doctor.rs new file mode 100644 index 00000000..7b05afc3 --- /dev/null +++ b/xtask/src/doctor.rs @@ -0,0 +1,86 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use clap::Parser; +use log::{info, warn}; + +use crate::util::{ROOTASROLE, detect_priv_bin, get_os}; + +#[derive(Debug, Parser)] +pub struct DoctorOptions { + /// Also check optional packaging/build tools + #[clap(long)] + pub full: bool, +} + +fn command_exists(command: &str) -> bool { + let Some(path) = env::var_os("PATH") else { + return false; + }; + + for dir in env::split_paths(&path) { + let candidate: PathBuf = dir.join(command); + if is_executable(&candidate) { + return true; + } + } + + false +} + +fn is_executable(path: &Path) -> bool { + if let Ok(metadata) = fs::metadata(path) { + if !metadata.is_file() { + return false; + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + return metadata.permissions().mode() & 0o111 != 0; + } + #[cfg(not(unix))] + { + return true; + } + } + false +} + +pub fn doctor(opts: &DoctorOptions) -> Result<(), anyhow::Error> { + let os = get_os(None)?; + info!("Detected target OS: {os}"); + + if let Some(priv_bin) = detect_priv_bin() { + info!("Privilege escalator: {}", priv_bin.display()); + } else { + warn!("No privilege escalator detected (dosr/sudo/doas)"); + } + + for command in ["cargo", "git"] { + if command_exists(command) { + info!("Found required command: {command}"); + } else { + warn!("Missing required command: {command}"); + } + } + + if opts.full { + for command in ["pandoc", "gzip", "upx"] { + if command_exists(command) { + info!("Found optional command: {command}"); + } else { + warn!("Missing optional command: {command}"); + } + } + } + + if fs::metadata(ROOTASROLE).is_ok() { + info!("Configuration file exists: {ROOTASROLE}"); + } else { + warn!("Configuration file does not exist yet: {ROOTASROLE}"); + } + + Ok(()) +} diff --git a/xtask/src/installer/build.rs b/xtask/src/installer/build.rs index a4cbdd4f..53cb3ccd 100644 --- a/xtask/src/installer/build.rs +++ b/xtask/src/installer/build.rs @@ -2,7 +2,10 @@ use std::{fs, process::Command}; use log::debug; -use crate::{installer::Toolchain, util::change_dir_to_git_root}; +use crate::{ + installer::Toolchain, + util::{change_dir_to_project_root, run_checked}, +}; use super::BuildOptions; @@ -22,17 +25,17 @@ fn build_binary( } args.extend(additionnal_args); debug!("Building {name} binary with args: {args:?}"); - Command::new("cargo").args(args).status()?; + run_checked( + Command::new("cargo").args(args), + &format!("build {name} binary"), + )?; Ok(()) } pub fn build(options: &BuildOptions) -> Result<(), anyhow::Error> { - change_dir_to_git_root()?; + change_dir_to_project_root()?; if options.clean_before { - Command::new("cargo") - .arg("clean") - .status() - .expect("failed to clean"); + run_checked(Command::new("cargo").arg("clean"), "clean build artifacts")?; } build_binary("dosr", options, vec!["--features", "finder"])?; build_binary("chsr", options, vec!["--features", "editor"])?; @@ -46,31 +49,34 @@ 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([ + run_checked( + Command::new("pandoc").args([ "-s", "-t", "man", "resources/man/en_US.md", "-o", "target/man/dosr.8", - ]) - .status()?; + ]), + "generate English manpage", + )?; fs::create_dir_all("target/man/fr")?; - Command::new("pandoc") - .args([ + run_checked( + Command::new("pandoc").args([ "-s", "-t", "man", "resources/man/fr_FR.md", "-o", "target/man/fr/dosr.8", - ]) - .status()?; + ]), + "generate French manpage", + )?; debug!("Compressing manpages"); - Command::new("gzip") - .args(["target/man/dosr.8", "target/man/fr/dosr.8"]) - .status()?; + run_checked( + Command::new("gzip").args(["target/man/dosr.8", "target/man/fr/dosr.8"]), + "compress manpages", + )?; debug!("Manpages built"); Ok(()) } diff --git a/xtask/src/installer/dependencies.rs b/xtask/src/installer/dependencies.rs index 00d08a61..16bf6340 100644 --- a/xtask/src/installer/dependencies.rs +++ b/xtask/src/installer/dependencies.rs @@ -1,132 +1,261 @@ -use std::process::ExitStatus; +use std::{borrow::Cow, collections::HashMap, path::Path, process::ExitStatus, sync::OnceLock}; use anyhow::Context; use capctl::CapState; use log::info; use nix::unistd::geteuid; +use serde::Deserialize; -use crate::{installer::OsTarget, util::get_os}; +use crate::{ + installer::OsTarget, + util::{self, detect_priv_bin, get_os, path_exe_from_env, run_checked, status_checked}, +}; use super::InstallDependenciesOptions; -fn update_package_manager(os: &OsTarget, priv_bin: Option<&String>) -> Result<(), anyhow::Error> { - let mut command = Vec::new(); - if is_priv_bin_necessary(os)? { - if let Some(priv_bin) = priv_bin { - command.push(priv_bin.as_str()); +#[derive(Debug, Deserialize)] +struct DependenciesManifest<'a> { + targets: HashMap, TargetDependencies<'a>>, +} + +#[derive(Debug, Deserialize)] +struct TargetDependencies<'a> { + #[serde(default)] + aliases: Vec>, + package_manager: PackageManager<'a>, + runtime: Vec>, + development: Vec>, +} + +#[derive(Debug, Deserialize)] +struct PackageManager<'a> { + refresh: Vec>, + install: Vec>, +} + +fn dependencies_manifest() -> Result<&'static DependenciesManifest<'static>, anyhow::Error> { + static MANIFEST: OnceLock, String>> = OnceLock::new(); + let manifest = MANIFEST.get_or_init(|| { + serde_json::from_str(include_str!("deps.json")) + .context("Failed to parse installer dependency manifest") + .map_err(|e| e.to_string()) + }); + match manifest { + Ok(manifest) => Ok(manifest), + Err(e) => Err(anyhow::anyhow!(e.clone())), + } +} + +const fn os_key(os: &OsTarget) -> &'static str { + match os { + OsTarget::Debian => "debian", + OsTarget::Ubuntu => "ubuntu", + OsTarget::RedHat => "redhat", + OsTarget::Fedora => "fedora", + OsTarget::OpenSUSE => "opensuse", + OsTarget::ArchLinux => "archlinux", + } +} + +fn os_from_key(key: &str) -> Option { + match key { + "debian" => Some(OsTarget::Debian), + "ubuntu" => Some(OsTarget::Ubuntu), + "redhat" => Some(OsTarget::RedHat), + "fedora" => Some(OsTarget::Fedora), + "opensuse" => Some(OsTarget::OpenSUSE), + "archlinux" => Some(OsTarget::ArchLinux), + _ => None, + } +} + +fn os_from_identifier(manifest: &DependenciesManifest<'_>, identifier: &str) -> Option { + let identifier = identifier.trim().to_ascii_lowercase(); + if let Some(target) = os_from_key(&identifier) + && manifest.targets.contains_key(identifier.as_str()) + { + return Some(target); + } + + manifest.targets.iter().find_map(|(key, target)| { + if target + .aliases + .iter() + .any(|alias| alias.as_ref() == identifier.as_str()) + { + os_from_key(key.as_ref()) } else { - return Err(anyhow::anyhow!("Privileged binary is required")); + None + } + }) +} + +pub fn os_target_from_identifiers<'a, I>(identifiers: I) -> Result, anyhow::Error> +where + I: IntoIterator, +{ + let manifest = dependencies_manifest()?; + for id in identifiers { + if let Some(target) = os_from_identifier(manifest, id) { + return Ok(Some(target)); } } + Ok(None) +} - match os { - OsTarget::Debian | OsTarget::Ubuntu => command.extend(&["apt-get", "update"]), - OsTarget::RedHat => command.extend(&["yum", "update", "-y"]), - OsTarget::ArchLinux => command.extend(&["pacman", "-Syu"]), - OsTarget::Fedora => command.extend(&["dnf", "update", "-y"]), +fn resolve_target<'a>( + manifest: &'a DependenciesManifest<'a>, + os: &OsTarget, +) -> Result<&'a TargetDependencies<'a>, anyhow::Error> { + let key = os_key(os); + if let Some(target) = manifest.targets.get(key) { + return Ok(target); } - std::process::Command::new(command[0]) - .args(&command[1..]) - .status() - .context("Failed to update package manager")?; - Ok(()) + manifest + .targets + .values() + .find(|target| target.aliases.iter().any(|alias| alias.as_ref() == key)) + .ok_or_else(|| anyhow::anyhow!("Unsupported OS target in deps.json: {key}")) } -const fn required_dependencies(os: &OsTarget) -> &'static [&'static str] { - match os { - OsTarget::Debian | OsTarget::Ubuntu => &["libpam0g", "libpcre2-8-0", "libseccomp-dev"], - OsTarget::RedHat => &["pcre2", "libseccomp"], - OsTarget::ArchLinux | OsTarget::Fedora => &["pam", "pcre2", "libseccomp"], +fn compose_command( + priv_bin: Option<&Path>, + base: &[Cow<'_, str>], +) -> Result, anyhow::Error> { + if base.is_empty() { + return Err(anyhow::anyhow!( + "Invalid package-manager command in deps.json" + )); + } + + let mut command = Vec::new(); + if is_priv_bin_necessary()? { + if let Some(priv_bin) = priv_bin { + if util::is_su_command(priv_bin) { + let shell_command = base + .iter() + .map(|arg| shell_quote(arg.as_ref())) + .collect::>() + .join(" "); + command.push(priv_bin.to_string_lossy().into_owned()); + command.push("-c".to_string()); + command.push(shell_command); + return Ok(command); + } else if util::is_run0_command(priv_bin) { + command.push(priv_bin.to_string_lossy().into_owned()); + command.push("--pipe".to_string()); + command.extend(base.iter().map(|s| s.as_ref().to_string())); + return Ok(command); + } + command.push(priv_bin.to_string_lossy().into_owned()); + } else { + return Err(anyhow::anyhow!("Privileged binary is required")); + } } + command.extend(base.iter().map(|s| s.as_ref().to_string())); + Ok(command) } -const fn development_dependencies(os: &OsTarget) -> &'static [&'static str] { - match os { - OsTarget::Debian | OsTarget::Ubuntu => &[ - "libpam0g-dev", - "libpcre2-dev", - "libclang-dev", - "pandoc", - "libseccomp-dev", - ], - OsTarget::RedHat => &[ - "pcre2-devel", - "clang-devel", - "openssl-devel", - "pam-devel", - "pandoc", - "libseccomp", - ], - OsTarget::Fedora => &[ - "clang-devel", - "openssl-devel", - "pam-devel", - "pandoc", - "libseccomp", - ], - OsTarget::ArchLinux => &["clang", "pkg-config", "pandoc", "libseccomp"], +fn shell_quote(arg: &str) -> String { + if arg + .chars() + .all(|c| c.is_ascii_alphanumeric() || "@%_+=:,./-".contains(c)) + { + arg.to_string() + } else { + format!("'{}'", arg.replace('\'', "'\\''")) } } -const fn get_dependencies(os: &OsTarget, dev: bool) -> &'static [&'static str] { +fn update_package_manager(os: &OsTarget, priv_bin: Option<&Path>) -> Result<(), anyhow::Error> { + let manifest = dependencies_manifest()?; + let target = resolve_target(manifest, os)?; + let command = compose_command(priv_bin, target.package_manager.refresh.as_slice())?; + log::info!( + "Updating package manager with command: {}", + command.join(" ") + ); + run_checked( + std::process::Command::new(&command[0]).args(&command[1..]), + "update package manager", + ) + .context("Failed to update package manager")?; + + Ok(()) +} + +fn get_dependencies<'a>( + manifest: &'a DependenciesManifest<'a>, + os: &OsTarget, + dev: bool, +) -> Result<&'a [Cow<'a, str>], anyhow::Error> { + let target = resolve_target(manifest, os)?; if dev { - development_dependencies(os) + Ok(target.development.as_slice()) } else { - required_dependencies(os) + Ok(target.runtime.as_slice()) } } -fn is_priv_bin_necessary(os: &OsTarget) -> Result { - if os == &OsTarget::ArchLinux { - Ok(!geteuid().is_root()) +fn is_priv_bin_necessary() -> Result { + if geteuid().is_root() { + // as long root own files/folders, it should not need capabilities. + return Ok(false); + } + let mut state = CapState::get_current()?; + if state.permitted.has(capctl::Cap::DAC_OVERRIDE) + && !state.effective.has(capctl::Cap::DAC_OVERRIDE) + { + state.effective.add(capctl::Cap::DAC_OVERRIDE); + state.set_current()?; + Ok(false) } else { - let mut state = CapState::get_current()?; - if state.permitted.has(capctl::Cap::DAC_OVERRIDE) - && !state.effective.has(capctl::Cap::DAC_OVERRIDE) - { - state.effective.add(capctl::Cap::DAC_OVERRIDE); - state.set_current()?; - Ok(false) - } else { - Ok(true) - } + Ok(true) } } pub fn install_dependencies( os: &OsTarget, deps: &[&str], - priv_bin: Option<&String>, + priv_bin: Option<&Path>, ) -> Result { - let mut command = Vec::new(); + let manifest = dependencies_manifest()?; + let target = resolve_target(manifest, os)?; - if is_priv_bin_necessary(os)? { - if let Some(priv_bin) = &priv_bin { - command.push(priv_bin.as_str()); - } else { - return Err(anyhow::anyhow!("Privileged binary is required")); - } - } - command.extend(match os { - OsTarget::Debian | OsTarget::Ubuntu => ["apt-get", "install", "-y"], - OsTarget::RedHat => ["yum", "install", "-y"], - OsTarget::Fedora => ["dnf", "install", "-y"], - OsTarget::ArchLinux => ["pacman", "-Syu", "--noconfirm"], - }); - command.extend(deps); - Ok(std::process::Command::new(command[0]) - .args(&command[1..]) - .status()?) + let mut command = compose_command(priv_bin, target.package_manager.install.as_slice())?; + command.extend(deps.iter().map(std::string::ToString::to_string)); + + status_checked( + std::process::Command::new(&command[0]).args(&command[1..]), + "install required packages", + ) } pub fn install(opts: &InstallDependenciesOptions) -> Result<(), anyhow::Error> { let os = get_os(opts.os.as_ref())?; - update_package_manager(&os, opts.priv_bin.as_ref())?; + let priv_bin = opts + .priv_bin + .clone() + .or_else(detect_priv_bin) + .and_then(|bin| { + path_exe_from_env( + &std::env::var_os("PATH") + .unwrap_or_default() + .to_string_lossy() + .split(':') + .collect::>(), + bin, + ) + }); + update_package_manager(&os, priv_bin.as_deref())?; // dependencies are : libpam and libpcre2 info!("Installing dependencies: libpam.so and libpcre2.so for running the application"); - install_dependencies(&os, get_dependencies(&os, opts.dev), opts.priv_bin.as_ref())?; + let manifest = dependencies_manifest()?; + let deps = get_dependencies(manifest, &os, opts.dev)?; + let deps: Vec<&str> = deps.iter().map(std::convert::AsRef::as_ref).collect(); + install_dependencies(&os, &deps, priv_bin.as_deref())?; info!("Dependencies installed successfully"); Ok(()) diff --git a/xtask/src/installer/deps.json b/xtask/src/installer/deps.json new file mode 100644 index 00000000..2a23c150 --- /dev/null +++ b/xtask/src/installer/deps.json @@ -0,0 +1,60 @@ +{ + "targets": { + "debian": { + "aliases": ["ubuntu"], + "package_manager": { + "refresh": ["apt-get", "update"], + "install": ["apt-get", "install", "-y"] + }, + "runtime": ["libpam0g", "libpcre2-8-0", "libseccomp-dev"], + "development": [ + "libpam0g-dev", + "libpcre2-dev", + "libclang-dev", + "pandoc", + "libseccomp-dev" + ] + }, + "fedora": { + "aliases": ["rockylinux", "rocky", "centos", "almalinux", "rhel", "redhat"], + "package_manager": { + "refresh": ["dnf", "update", "-y"], + "install": ["dnf", "install", "-y"] + }, + "runtime": ["pam", "pcre2", "libseccomp"], + "development": [ + "pcre2-devel", + "clang-devel", + "openssl-devel", + "pam-devel", + "pandoc", + "libseccomp-devel" + ] + }, + "opensuse": { + "aliases": ["suse"], + "package_manager": { + "refresh": ["zypper", "refresh"], + "install": ["zypper", "install", "-y"] + }, + "runtime": ["pam", "pcre2", "libseccomp"], + "development": [ + "pcre2-devel", + "clang-devel", + "libopenssl-devel", + "pam-devel", + "pandoc", + "libseccomp-devel" + ] + }, + "archlinux": { + "aliases": ["arch"], + "package_manager": { + "refresh": ["pacman", "-Syu"], + "install": ["pacman", "-Syu", "--noconfirm"] + }, + "runtime": ["pam", "pcre2", "libseccomp"], + "development": ["clang", "pkg-config", "pandoc", "libseccomp"] + } + } +} diff --git a/xtask/src/installer/install.rs b/xtask/src/installer/install.rs index 62dfa7e3..44077473 100644 --- a/xtask/src/installer/install.rs +++ b/xtask/src/installer/install.rs @@ -6,19 +6,33 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use capctl::{Cap, CapSet}; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use nix::NixPath; use nix::sys::stat::{Mode, fchmod}; use nix::unistd::{Gid, Uid}; use strum::EnumIs; use crate::installer::Profile; -use crate::util::{BOLD, RED, RST, change_dir_to_git_root, detect_priv_bin}; +use crate::util::{ + BOLD, RED, RST, change_dir_to_project_root, detect_priv_bin, is_run0_command, is_su_command, + run_checked, +}; use anyhow::{Context, anyhow}; use super::{CHSR_DEST, RAR_BIN_PATH, SR_DEST}; use crate::util::cap_clear; +fn shell_quote(arg: &str) -> String { + if arg + .chars() + .all(|c| c.is_ascii_alphanumeric() || "@%_+=:,./-".contains(c)) + { + arg.to_string() + } else { + format!("'{}'", arg.replace('\'', "'\\''")) + } +} + fn copy_executables(profile: Profile) -> Result<(), anyhow::Error> { let chsr_dest = Path::new(RAR_BIN_PATH).join(CHSR_DEST); let sr_dest = Path::new(RAR_BIN_PATH).join(SR_DEST); @@ -31,8 +45,12 @@ fn copy_executables(profile: Profile) -> Result<(), anyhow::Error> { "Copying files {}/target/{}/dosr to {} and {}", cwd, profile, - sr_dest.to_str().unwrap(), - chsr_dest.to_str().unwrap() + sr_dest + .to_str() + .expect("Failed to convert sr_dest to string"), + chsr_dest + .to_str() + .expect("Failed to convert chsr_dest to string") ); let s_sr = format!("{cwd}/target/{profile}/dosr"); let sr = Path::new(&s_sr); @@ -48,10 +66,30 @@ fn copy_executables(profile: Profile) -> Result<(), anyhow::Error> { fs::copy(sr, format!("{s_sr}.tmp"))?; debug!("Copying chsr to chsr.tmp"); fs::copy(chsr, format!("{s_chsr}.tmp"))?; - debug!("Renaming sr to /usr/bin/dosr"); - fs::rename(sr, sr_dest)?; - debug!("Renaming chsr to /usr/bin/chsr"); - fs::rename(chsr, chsr_dest)?; + debug!("Moving sr to /usr/bin/dosr"); + match fs::rename(sr, &sr_dest) { + Ok(()) => {} + Err(e) if e.raw_os_error() == Some(18) => { + // EXDEV (errno 18): Invalid cross-device link + // Fall back to copy + remove for cross-filesystem operations + debug!("Cross-device link detected, using copy+remove instead"); + fs::copy(sr, &sr_dest)?; + fs::remove_file(sr)?; + } + Err(e) => return Err(e.into()), + } + debug!("Moving chsr to /usr/bin/chsr"); + match fs::rename(chsr, &chsr_dest) { + Ok(()) => {} + Err(e) if e.raw_os_error() == Some(18) => { + // EXDEV (errno 18): Invalid cross-device link + // Fall back to copy + remove for cross-filesystem operations + debug!("Cross-device link detected, using copy+remove instead"); + fs::copy(chsr, &chsr_dest)?; + fs::remove_file(chsr)?; + } + Err(e) => return Err(e.into()), + } debug!("Renaming sr.tmp to sr"); fs::rename(format!("{s_sr}.tmp"), sr)?; debug!("Renaming chsr.tmp to chsr"); @@ -93,7 +131,7 @@ fn copy_docs() -> Result<(), anyhow::Error> { })?; let lang = file.parent(); if lang.is_some_and(|p| !NixPath::is_empty(p)) { - let lang = lang.unwrap(); + let lang = lang.expect("Failed to get Lang path"); //println!("lang: {:?}", lang); let lang = lang.file_name().ok_or_else(|| { exit_directory().expect("Failed to exit directory"); @@ -105,15 +143,19 @@ fn copy_docs() -> Result<(), anyhow::Error> { })?; let dest = format!("/usr/share/man/{lang}/man8/{file_name}"); debug!("Copying file: {} to {dest}", file.display()); - fs::copy(&file, &dest).inspect_err(|_| { - exit_directory().expect("Failed to exit directory"); - })?; + fs::copy(&file, &dest) + .inspect_err(|_| { + exit_directory().expect("Failed to exit directory"); + }) + .context(format!("Unable to copy {} to {dest}", file.display()))?; } else { let dest = format!("/usr/share/man/man8/{file_name}"); - debug!("Copying file: {} to {}", file.display(), dest); - fs::copy(&file, &dest).inspect_err(|_| { - exit_directory().expect("Failed to exit directory"); - })?; + debug!("Copying file: {} to {dest}", file.display()); + fs::copy(&file, &dest) + .inspect_err(|_| { + exit_directory().expect("Failed to exit directory"); + }) + .context(format!("Unable to copy {} to {dest}", file.display()))?; } } exit_directory()?; @@ -164,8 +206,9 @@ fn cap_effective(state: &mut capctl::CapState, cap: Cap) -> Result<(), anyhow::E Ok(()) } +#[allow(clippy::too_many_lines)] pub fn install( - priv_exe: Option<&String>, + priv_exe: Option<&Path>, profile: Profile, clean_after: bool, copy: bool, @@ -194,7 +237,7 @@ pub fn install( let priv_bin = detect_priv_bin(); let priv_exe = priv_exe - .or(priv_bin.as_ref()) + .or(priv_bin.as_deref()) .context("Privileged binary is required") .map_err(|_| { anyhow::Error::msg(format!( @@ -202,30 +245,60 @@ pub fn install( current_exe() .unwrap_or_else(|_| PathBuf::from_str("the command").unwrap()) .to_str() - .unwrap() + .expect("Failed to convert current exe path to string") )) })?; - change_dir_to_git_root()?; // change to the root of the project before elevating privileges + change_dir_to_project_root()?; // change to the root of the project before elevating privileges unsafe { env::set_var("ROOTASROLE_INSTALLER_NESTED", "1") }; log::warn!("Elevating privileges..."); - std::process::Command::new(priv_exe) - .arg("-E") - .arg( - current_exe()? - .to_str() - .context("Failed to get current exe path")?, - ) - .arg("install") - .status() + let current_exe_path = current_exe()?; + let current_exe_str = current_exe_path + .to_str() + .context("Failed to get current exe path")? + .to_string(); + let mut command = std::process::Command::new(priv_exe); + if is_su_command(priv_exe) { + let mut shell_cmd_args = vec![ + shell_quote(¤t_exe_str), + "install".to_string(), + "--nested-install".to_string(), + ]; + if profile.is_debug() { + shell_cmd_args.push("--debug".to_string()); + } + let shell_cmd = shell_cmd_args.join(" "); + command.arg("-c").arg(shell_cmd); + } else if is_run0_command(priv_exe) { + let mut shell_cmd_args = vec![ + shell_quote(¤t_exe_str), + "install".to_string(), + "--nested-install".to_string(), + ]; + if profile.is_debug() { + shell_cmd_args.push("--debug".to_string()); + } + let shell_cmd = shell_cmd_args.join(" "); + command.arg("--pipe").arg("sh").arg("-c").arg(shell_cmd); + } else { + command + .arg(¤t_exe_str) + .arg("install") + .arg("--nested-install"); + if profile.is_debug() { + command.arg("--debug"); + } + } + run_checked(&mut command, "run privileged installer") .context("Failed to run privileged binary") .map_err(|e| { error!("{e}"); anyhow::Error::msg(format!( "Failed to run privileged binary. Please run {} as an administrator.", current_exe() - .unwrap_or_else(|_| PathBuf::from_str("the command").unwrap()) + .unwrap_or_else(|_| PathBuf::from_str("the command") + .expect("Failed to get current exe path")) .to_str() - .unwrap() + .expect("Failed to convert current exe path to string") )) })?; return Ok(Elevated::Yes); @@ -238,7 +311,9 @@ pub fn install( // cp target/{release}/dosr,chsr,capable /usr/bin copy_executables(profile).context("Failed to copy sr and chsr files")?; - copy_docs().context("Failed to copy documentation files")?; + if let Err(e) = copy_docs() { + warn!("Unable to copy docs : {e}"); + } // drop dac_override cap_clear(&mut state).context("Failed to drop effective DAC_OVERRIDE")?; @@ -265,10 +340,11 @@ pub fn install( cap_clear(&mut state).context("Failed to drop effective capabilities")?; if clean_after { - std::process::Command::new("cargo") - .args(["clean"]) - .status() - .context("Failed to clean the project")?; + run_checked( + std::process::Command::new("cargo").args(["clean"]), + "clean project", + ) + .context("Failed to clean the project")?; } Ok(Elevated::No) } diff --git a/xtask/src/installer/mod.rs b/xtask/src/installer/mod.rs index 446d2cd9..6f07e562 100644 --- a/xtask/src/installer/mod.rs +++ b/xtask/src/installer/mod.rs @@ -4,6 +4,7 @@ pub mod install; mod uninstall; use std::fmt::Write; +use std::path::PathBuf; use std::str::FromStr; use std::{collections::VecDeque, fmt::Display}; @@ -15,19 +16,25 @@ use strum::{Display, EnumIs, EnumString}; use anyhow::anyhow; use log::debug; +use crate::util::path_exe_from_env; use crate::{ configure, - util::{OsTarget, detect_priv_bin, get_os}, + util::{OsTarget, detect_priv_bin, get_os, is_dry_run}, }; pub const RAR_BIN_PATH: &str = env!("RAR_BIN_PATH"); pub const SR_DEST: &str = "dosr"; pub const CHSR_DEST: &str = "chsr"; #[derive(Debug, Parser, Clone)] +#[allow(clippy::struct_excessive_bools)] pub struct InstallOptions { #[clap(flatten)] pub build_opts: BuildOptions, + /// Hidden flag used internally when install re-executes itself through a privilege escalator + #[clap(long, hide = true)] + pub nested_install: bool, + /// The OS target for PAM configuration and dependencies installation (if -i is set) /// By default, it tries to autodetect it #[clap(long, short)] @@ -44,10 +51,6 @@ pub struct InstallOptions { /// Clean the target directory after installing #[clap(long, short = 'a')] pub clean_after: bool, - - /// The binary to elevate privileges - #[clap(long, short = 'p')] - pub priv_bin: Option, } #[derive(Debug, Parser)] @@ -66,8 +69,8 @@ pub struct InstallDependenciesOptions { pub dev: bool, /// The binary to elevate privileges - #[clap(long, short = 'p')] - pub priv_bin: Option, + #[clap(long, short = 'p', visible_alias = "privbin")] + pub priv_bin: Option, } #[derive(Debug, Parser)] @@ -76,6 +79,10 @@ pub struct UninstallOptions { #[clap(long, short = 'c')] pub clean_config: bool, + /// Apply filesystem changes (required) + #[clap(long)] + pub apply: bool, + pub kind: UninstallKind, } @@ -97,7 +104,8 @@ pub enum Profile { #[derive(Debug, Parser, Clone)] pub struct BuildOptions { /// The binary to elevate privileges - pub privbin: Option, + #[clap(long, short = 'p', visible_alias = "privbin")] + pub priv_bin: Option, /// Build the target with debug profile (default is release) #[clap(short = 'd', long = "debug", default_value_t = Profile::Release, default_missing_value = "debug", num_args = 0)] @@ -202,7 +210,7 @@ impl FromStr for Toolchain { } let channel = parts .pop_front() - .unwrap() + .expect("Failed to get channel part from toolchain string") .to_lowercase() .as_str() .parse::()?; @@ -228,22 +236,54 @@ impl FromStr for Toolchain { } pub fn configure(os: Option) -> Result<(), anyhow::Error> { + if is_dry_run() { + debug!("Dry-run mode: skipping configure changes"); + return Ok(()); + } configure::configure(os) } pub fn dependencies(opts: &InstallDependenciesOptions) -> Result<(), anyhow::Error> { + if is_dry_run() { + debug!("Dry-run mode: skipping dependencies installation"); + return Ok(()); + } dependencies::install(opts) } pub fn install(opts: &InstallOptions) -> Result<(), anyhow::Error> { + if is_dry_run() { + debug!("Dry-run mode: skipping install changes"); + return Ok(()); + } + if opts.nested_install { + unsafe { std::env::set_var("ROOTASROLE_INSTALLER_NESTED", "1") }; + } else { + unsafe { std::env::remove_var("ROOTASROLE_INSTALLER_NESTED") }; + } let os = get_os(opts.os.as_ref())?; + let priv_bin = opts + .build_opts + .priv_bin + .clone() + .or_else(detect_priv_bin) + .and_then(|bin| { + path_exe_from_env( + &std::env::var_os("PATH") + .unwrap_or_default() + .to_string_lossy() + .split(':') + .collect::>(), + bin, + ) + }); if opts.install_dependencies { debug!("Installing dependencies"); dependencies(&InstallDependenciesOptions { os: Some(os.clone()), install_dependencies: true, dev: opts.build, - priv_bin: opts.build_opts.privbin.clone().or_else(detect_priv_bin), + priv_bin: priv_bin.clone(), })?; } if opts.build { @@ -251,7 +291,7 @@ pub fn install(opts: &InstallOptions) -> Result<(), anyhow::Error> { build(&opts.build_opts)?; } if install::install( - opts.priv_bin.as_ref(), + priv_bin.as_deref(), opts.build_opts.profile, opts.clean_after, true, @@ -269,5 +309,9 @@ pub fn build(opts: &BuildOptions) -> Result<(), anyhow::Error> { } pub fn uninstall(opts: &UninstallOptions) -> Result<(), anyhow::Error> { + if is_dry_run() { + debug!("Dry-run mode: skipping uninstall changes"); + return Ok(()); + } uninstall::uninstall(opts) } diff --git a/xtask/src/installer/uninstall.rs b/xtask/src/installer/uninstall.rs index c8016ec2..48ad9c8e 100644 --- a/xtask/src/installer/uninstall.rs +++ b/xtask/src/installer/uninstall.rs @@ -29,7 +29,7 @@ pub fn uninstall(opts: &UninstallOptions) -> Result<(), anyhow::Error> { } for error in errors { if let Err(e) = error { - warn!("{}: {}", e, e.source().unwrap()); + warn!("{}: {}", e, e.source().expect("Error should have a source")); } } Ok(()) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 80029234..790afe80 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,17 +1,23 @@ mod configure; mod deploy; +mod doctor; mod installer; pub mod util; -use Command::{Build, Configure, Dependencies, Deploy, Install, Uninstall}; +use Command::{Build, Configure, Dependencies, Deploy, Doctor, Install, Uninstall}; + use std::process::exit; use clap::Parser; -use log::error; +use log::{debug, error, info}; use util::OsTarget; #[derive(Debug, Parser)] pub struct Options { + /// Print planned actions without executing mutating steps + #[clap(long, global = true)] + dry_run: bool, + #[clap(subcommand)] command: Command, } @@ -33,13 +39,24 @@ enum Command { Uninstall(installer::UninstallOptions), #[cfg(feature = "deploy")] Deploy(deploy::MakeOptions), + Doctor(doctor::DoctorOptions), } fn main() { env_logger::builder() - .default_format() - .format_module_path(true) + .filter_level(log::LevelFilter::Info) + .format_module_path(false) + .format_file(false) + .format_source_path(false) + .format_target(false) .init(); + debug!( + "Starting xtask with arguments: {:?}", + std::env::args().collect::>() + ); + if std::env::var_os("ROOTASROLE_INSTALLER_NESTED").is_some() { + info!("nested install is enabled"); + } let opts = Options::parse(); let ret = match opts.command { Dependencies(opts) => installer::dependencies(&opts), @@ -48,6 +65,7 @@ fn main() { Configure { os } => installer::configure(os), Uninstall(opts) => installer::uninstall(&opts), Deploy(opts) => deploy::deploy(&opts), + Doctor(opts) => doctor::doctor(&opts), }; if let Err(e) = ret { diff --git a/xtask/src/util.rs b/xtask/src/util.rs index aa8ae2e9..b83e609f 100644 --- a/xtask/src/util.rs +++ b/xtask/src/util.rs @@ -4,8 +4,9 @@ use std::{ fs::{self, File}, io, os::{fd::AsRawFd, unix::fs::MetadataExt}, - path::Path, - process::Command, + path::{Path, PathBuf}, + process::{Command, ExitStatus, Output}, + sync::atomic::{AtomicBool, Ordering}, }; use anyhow::{Context, anyhow}; @@ -13,7 +14,7 @@ use capctl::Cap; use capctl::CapState; use chrono::Duration; use clap::ValueEnum; -use log::debug; +use log::{debug, info}; use nix::libc::{FS_IOC_GETFLAGS, FS_IOC_SETFLAGS}; use serde::{Deserialize, Serialize, de}; use serde_json::Value; @@ -30,15 +31,41 @@ pub enum OsTarget { RedHat, #[clap(alias = "fed")] Fedora, + #[clap(alias = "suse")] + OpenSUSE, #[clap(alias = "arch")] ArchLinux, } impl OsTarget { + fn os_release_identifiers(content: &str) -> Vec { + content + .lines() + .filter_map(|line| line.split_once('=')) + .filter_map(|(key, value)| { + if key == "ID" || key == "ID_LIKE" { + Some(value) + } else { + None + } + }) + .flat_map(|value| value.trim_matches('"').split_whitespace()) + .map(str::to_ascii_lowercase) + .collect() + } /// # Errors /// /// Will return an error if the OS cannot be detected or is unsupported pub fn detect() -> Result { + if let Ok(os_release) = std::fs::read_to_string("/etc/os-release") { + let identifiers = Self::os_release_identifiers(&os_release); + if let Some(target) = crate::installer::dependencies::os_target_from_identifiers( + identifiers.iter().map(std::string::String::as_str), + )? { + return Ok(target); + } + } + for file in glob::glob("/etc/*-release")? { let file = file?; let os = std::fs::read_to_string(&file)?.to_ascii_lowercase(); @@ -62,6 +89,7 @@ pub const RST: &str = "\x1B[0m"; pub const BOLD: &str = "\x1B[1m"; pub const UNDERLINE: &str = "\x1B[4m"; pub const RED: &str = "\x1B[31m"; +pub const GREEN: &str = "\x1B[32m"; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SettingsFile { @@ -256,6 +284,7 @@ pub struct Opt { const FS_IMMUTABLE_FL: u32 = 0x0000_0010; pub const ROOTASROLE: &str = env!("RAR_CFG_PATH"); +static DRY_RUN: AtomicBool = AtomicBool::new(false); #[derive(Debug, EnumIs)] pub enum ImmutableLock { @@ -315,7 +344,7 @@ pub fn convert_string_to_duration(s: &str) -> Result, fn immutable_required_privileges(file: &File, effective: bool) -> Result<(), capctl::Error> { //get file owner - let metadata = file.metadata().unwrap(); + let metadata = file.metadata().expect("Failed to get file metadata"); let uid = metadata.uid(); let gid = metadata.gid(); immutable_effective(effective)?; @@ -343,17 +372,107 @@ fn read_or_dac_override(effective: bool) -> Result<(), capctl::Error> { /// # Errors /// -/// Will return an error if the current directory is not a git repository or if git command fails -pub fn change_dir_to_git_root() -> Result<(), anyhow::Error> { - let output = Command::new("git") - .args(["rev-parse", "--show-toplevel"]) - .output()?; - let git_root = String::from_utf8(output.stdout)?.trim().to_string(); - debug!("Changing directory to git root: {git_root}"); - std::env::set_current_dir(git_root)?; +/// Will return an error if the current directory is not a cargo project or if cargo command fails +pub fn change_dir_to_project_root() -> Result<(), anyhow::Error> { + // check if current directory is our code repo by looking for the Cargo.toml file + let output = output_checked( + Command::new("cargo").args(["locate-project", "--workspace"]), + "check if current directory is a cargo workspace", + )?; + let json = String::from_utf8(output.stdout)?; + let value: Value = serde_json::from_str(&json)?; + let manifest_path = Path::new( + value + .get("root") + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("Failed to parse cargo locate-project output"))?, + ); + + std::env::set_current_dir( + manifest_path + .parent() + .ok_or_else(|| anyhow!("Failed to get parent directory of Cargo.toml"))?, + )?; Ok(()) } +pub fn set_dry_run(enabled: bool) { + DRY_RUN.store(enabled, Ordering::Relaxed); +} + +#[must_use] +pub fn is_dry_run() -> bool { + DRY_RUN.load(Ordering::Relaxed) +} + +/// # Errors +/// +/// Will return an error if the command fails to execute or exits with a non-zero code +pub fn status_checked(command: &mut Command, action: &str) -> Result { + let status = command + .status() + .with_context(|| format!("Failed to {action}: {command:?}"))?; + if !status.success() { + anyhow::bail!("Failed to {action}: {command:?} exited with status {status}"); + } + Ok(status) +} + +fn shell_quote(arg: &str) -> String { + if arg.is_empty() { + "''".to_string() + } else if !arg.contains(|c: char| c.is_whitespace() || c == '\'' || c == '"') { + arg.to_string() + } else { + format!("'{}'", arg.replace('\'', "'\\''")) + } +} + +fn shell_quote_command(command: &Command) -> String { + format!( + "{} {}", + command.get_program().to_string_lossy(), + command + .get_args() + .map(|arg| shell_quote(arg.to_string_lossy().as_ref())) + .collect::>() + .join(" ") + ) +} + +/// # Errors +/// +/// Will return an error if the command fails to execute or exits with a non-zero code +pub fn run_checked(command: &mut Command, action: &str) -> Result<(), anyhow::Error> { + log_command_execution(command, action); + let _ = status_checked(command, action)?; + Ok(()) +} + +fn log_command_execution(command: &Command, action: &str) { + info!( + "{BOLD}Running:{RED} {}{RST}\n{BOLD} Objective -->{RST}{GREEN} {}{RST}", + shell_quote_command(command), + action + ); +} + +/// # Errors +/// +/// Will return an error if the command fails to execute or exits with a non-zero code +pub fn output_checked(command: &mut Command, action: &str) -> Result { + let output = command + .output() + .with_context(|| format!("Failed to {action}: {command:?}"))?; + if !output.status.success() { + anyhow::bail!( + "Failed to {action}: {command:?} exited with status {}", + output.status + ); + } + Ok(output) +} + /// Set or unset the immutable flag on a file /// # Arguments /// * `file` - The file to set the immutable flag on @@ -450,14 +569,16 @@ pub fn get_os(os: Option<&OsTarget>) -> Result { } #[must_use] -pub fn detect_priv_bin() -> Option { +pub fn detect_priv_bin() -> Option { // is /usr/bin/dosr exist ? if std::fs::metadata("/usr/bin/dosr").is_ok() { - Some("/usr/bin/dosr".to_string()) + Some("/usr/bin/dosr".into()) } else if std::fs::metadata("/usr/bin/sudo").is_ok() { - Some("/usr/bin/sudo".to_string()) + Some("/usr/bin/sudo".into()) } else if std::fs::metadata("/usr/bin/doas").is_ok() { - Some("/usr/bin/doas".to_string()) + Some("/usr/bin/doas".into()) + } else if std::fs::metadata("/usr/bin/please").is_ok() { + Some("/usr/bin/please".into()) } else { None } @@ -470,3 +591,27 @@ pub fn cap_clear(state: &mut capctl::CapState) -> Result<(), anyhow::Error> { state.set_current()?; Ok(()) } + +#[must_use] +pub fn is_su_command(priv_bin: &Path) -> bool { + priv_bin.file_name().is_some_and(|name| name == "su") +} + +#[must_use] +pub fn is_run0_command(priv_bin: &Path) -> bool { + priv_bin.file_name().is_some_and(|name| name == "run0") +} + +pub fn path_exe_from_env>(env_path: &[&str], exe_name: P) -> Option { + env_path.iter().find_map(|dir| { + let full_path = Path::new(dir).join(&exe_name); + debug!("Checking path: {}", full_path.display()); + full_path.is_file().then_some(full_path).and_then(|path| { + if path.is_symlink() { + fs::read_link(path).ok() + } else { + path.canonicalize().ok() + } + }) + }) +}