rootasrole-core-3.2.0/.cargo_vcs_info.json0000644000000001500000000000100141510ustar { "git": { "sha1": "ec58eb7f0ce587b081d7945b494cb841a4a3e7b7" }, "path_in_vcs": "rar-common" }rootasrole-core-3.2.0/Cargo.lock0000644000001025130000000000100121320ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bon" version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537c317ddf588aab15c695bf92cf55dec159b93221c074180ca3e0e5a94da415" dependencies = [ "bon-macros", "rustversion", ] [[package]] name = "bon-macros" version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca5abbf2d4a4c6896197c9de13d6d7cb7eff438c63dacde1dde980569cb00248" dependencies = [ "darling", "ident_case", "prettyplease", "proc-macro2", "quote", "rustversion", "syn 2.0.106", ] [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "capctl" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a6e71767585f51c2a33fed6d67147ec0343725fc3c03bf4b89fe67fede56aa5" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", ] [[package]] name = "cbor4ii" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "189a2a2e5eec2f203b2bb8bc4c2db55c7253770d2c6bf3ae5f79ace5a15c305f" dependencies = [ "serde", ] [[package]] name = "cc" version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "jobserver", "libc", "shlex", ] [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "const_panic" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb8a602185c3c95b52f86dc78e55a6df9a287a7a93ddbcf012509930880cf879" dependencies = [ "typewit", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "darling" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", "syn 2.0.106", ] [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", "syn 2.0.106", ] [[package]] name = "deranged" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] [[package]] name = "derivative" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "env_filter" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", ] [[package]] name = "env_logger" version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", "jiff", "log", ] [[package]] name = "error-chain" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ "version_check", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi", ] [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hostname" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ "libc", "match_cfg", "winapi", ] [[package]] name = "iana-time-zone" version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", "serde", ] [[package]] name = "jiff-static" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "jobserver" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ "getrandom", "libc", ] [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "konst" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" dependencies = [ "const_panic", "konst_kernel", "konst_proc_macros", "typewit", ] [[package]] name = "konst_kernel" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" dependencies = [ "typewit", ] [[package]] name = "konst_proc_macros" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linked_hash_set" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bae85b5be22d9843c80e5fc80e9b64c8a3b1f98f867c709956eca3efff4e92e2" dependencies = [ "linked-hash-map", ] [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "match_cfg" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ "regex-automata 0.1.10", ] [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.9.3", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_threads" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "pcre2" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be55c43ac18044541d58d897e8f4c55157218428953ebd39d86df3ba0286b2b" dependencies = [ "libc", "log", "pcre2-sys", ] [[package]] name = "pcre2-sys" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "550f5d18fb1b90c20b87e161852c10cde77858c3900c5059b5ad2a1449f11d8a" dependencies = [ "cc", "libc", "pkg-config", ] [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "prettyplease" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn 2.0.106", ] [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rootasrole-core" version = "3.2.0" dependencies = [ "bitflags 2.9.3", "bon", "capctl", "cbor4ii", "chrono", "derivative", "env_logger", "glob", "hex", "konst", "libc", "linked_hash_set", "log", "nix", "once_cell", "pcre2", "semver", "serde", "serde_json", "sha2", "shell-words", "strum", "syslog", "test-log", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "serde_json" version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shell-words" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", "syn 2.0.106", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syslog" version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc7e95b5b795122fafe6519e27629b5ab4232c73ebb2428f568e82b1a457ad3" dependencies = [ "error-chain", "hostname", "libc", "log", "time", ] [[package]] name = "test-log" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e33b98a582ea0be1168eba097538ee8dd4bbe0f2b01b22ac92ea30054e5be7b" dependencies = [ "env_logger", "test-log-macros", "tracing-subscriber", ] [[package]] name = "test-log-macros" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451b374529930d7601b1eef8d32bc79ae870b6079b069401709c2a8bf9e75f36" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "thread_local" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] [[package]] name = "time" version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", ] [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", "sharded-slab", "thread_local", "tracing", "tracing-core", "tracing-log", ] [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "typewit" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dd91acc53c592cb800c11c83e8e7ee1d48378d05cfa33b5474f5f80c5b236bf" dependencies = [ "typewit_proc_macros", ] [[package]] name = "typewit_proc_macros" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "windows-interface" version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "windows-link" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ "windows-link", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.3", ] rootasrole-core-3.2.0/Cargo.toml0000644000000052630000000000100121610ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "rootasrole-core" version = "3.2.0" authors = ["Eddie Billoir "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "This core crate for the RootAsRole project." homepage = "https://lechatp.github.io/RootAsRole/" readme = false keywords = [ "sudo", "capabilities", "rbac", "linux", "security", ] license = "LGPL-3.0-or-later" repository = "https://github.com/LeChatP/RootAsRole" [features] default = [ "pcre2", "glob", ] finder = [ "pcre2", "glob", ] glob = ["dep:glob"] pcre2 = ["dep:pcre2"] [lib] name = "rootasrole_core" path = "src/lib.rs" [dependencies.bitflags] version = "2.9" [dependencies.bon] version = "3" features = ["experimental-overwritable"] [dependencies.capctl] version = "0.2" [dependencies.cbor4ii] version = "1.0" features = [ "serde", "serde1", "use_std", ] [dependencies.chrono] version = "0.4" [dependencies.derivative] version = "2.2" [dependencies.env_logger] version = "0.11" [dependencies.glob] version = "0.3" optional = true [dependencies.hex] version = "0.4" [dependencies.konst] version = "0.3" [dependencies.libc] version = "0.2" [dependencies.linked_hash_set] version = "0.1" [dependencies.log] version = "0.4" [dependencies.nix] version = "0.29" features = [ "user", "process", "signal", "fs", "hostname", ] [dependencies.once_cell] version = "1.20" [dependencies.pcre2] version = "0.2" optional = true [dependencies.semver] version = "1.0" features = ["serde"] [dependencies.serde] version = "1.0" features = [ "rc", "derive", ] [dependencies.serde_json] version = "1.0" [dependencies.sha2] version = "0.10" [dependencies.shell-words] version = "1.1" [dependencies.strum] version = "0.26" features = ["derive"] [dependencies.syslog] version = "6.0" [dev-dependencies.env_logger] version = "0.11" [dev-dependencies.log] version = "0.4" [dev-dependencies.test-log] version = "0.2" [build-dependencies.serde] version = "1.0" features = [ "rc", "derive", ] [build-dependencies.serde_json] version = "1.0" [lints.rust.unexpected_cfgs] level = "allow" priority = 0 check-cfg = ["cfg(tarpaulin_include)"] rootasrole-core-3.2.0/Cargo.toml.orig000064400000000000000000000027621046102023000156430ustar 00000000000000[package] name = "rootasrole-core" version = "3.2.0" edition = "2021" description = "This core crate for the RootAsRole project." license = "LGPL-3.0-or-later" authors = ["Eddie Billoir "] repository = "https://github.com/LeChatP/RootAsRole" homepage = "https://lechatp.github.io/RootAsRole/" keywords = ["sudo", "capabilities", "rbac", "linux", "security"] [dependencies] libc = "0.2" strum = { version = "0.26", features = ["derive"] } semver = { version = "1.0", features = ["serde"] } nix = { version = "0.29", features = ["user","process", "signal", "fs", "hostname"] } capctl = "0.2" pcre2 = { version = "0.2", optional = true } serde = { version = "1.0", features=["rc", "derive"] } serde_json = "1.0" glob = { version = "0.3", optional = true } bitflags = { version = "2.9" } shell-words = "1.1" linked_hash_set = { version = "0.1" } derivative = "2.2" sha2 = "0.10" chrono = "0.4" once_cell = "1.20" hex = "0.4" log = "0.4" syslog = "6.0" env_logger = "0.11" bon = { version = "3", features = ["experimental-overwritable"] } cbor4ii = { version = "1.0", features = ["serde", "serde1", "use_std"] } konst = "0.3" [dev-dependencies] log = "0.4" env_logger = "0.11" test-log = { version = "0.2" } [build-dependencies] serde = { version = "1.0", features=["rc", "derive"] } serde_json = "1.0" [features] default = ["pcre2", "glob"] pcre2 = ["dep:pcre2"] glob = ["dep:glob"] finder = ["pcre2", "glob"] [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] } rootasrole-core-3.2.0/src/api.rs000064400000000000000000000224251046102023000146600ustar 00000000000000use std::sync::Mutex; use capctl::CapSet; #[cfg(feature = "finder")] use log::debug; #[cfg(feature = "finder")] use serde_json::Value; use strum::EnumIs; #[cfg(feature = "finder")] use crate::database::finder::{ActorMatchMin, Cred, ExecSettings, TaskMatch}; #[cfg(feature = "finder")] use crate::database::FilterMatcher; use crate::database::{ actor::SActor, structs::{SConfig, SRole, STask}, }; use once_cell::sync::Lazy; static API: Lazy> = Lazy::new(|| Mutex::new(PluginManager::new())); // Define a trait for the plugin pub trait Plugin { fn initialize(&self); fn cleanup(&self); } #[derive(Debug, PartialEq, Eq, EnumIs)] pub enum PluginResultAction { Override, // The result of this plugin ends the algorithm to return the plugin result Edit, // The result of this plugin modify the result, algorithm continues Ignore, // The result of this plugin is ignored, algorithm continues } #[derive(Debug, PartialEq, Eq, EnumIs)] pub enum PluginResult { Deny, // The plugin denies the action Neutral, // The plugin has no opinion on the action } pub type ConfigLoaded = fn(config: &SConfig); #[cfg(feature = "finder")] pub type RoleMatcher = fn( role: &SRole, user: &Cred, filter: &Option, command: &[String], matcher: &mut TaskMatch, ) -> PluginResultAction; #[cfg(feature = "finder")] pub type TaskMatcher = fn( task: &STask, user: &Cred, command: &[String], matcher: &mut TaskMatch, ) -> PluginResultAction; #[cfg(feature = "finder")] pub type UserMatcher = fn(role: &SRole, user: &Cred, user_struct: &Value) -> ActorMatchMin; pub type RoleInformation = fn(role: &SRole) -> Option; pub type ActorInformation = fn(actor: &SActor) -> Option; pub type TaskInformation = fn(task: &STask) -> Option; #[cfg(feature = "finder")] pub type DutySeparation = fn(role: &SRole, actor: &Cred) -> PluginResult; #[cfg(feature = "finder")] pub type TaskSeparation = fn(task: &STask, actor: &Cred) -> PluginResult; pub type CapsFilter = fn(task: &STask, capabilities: &mut CapSet) -> PluginResultAction; #[cfg(feature = "finder")] pub type ExecutionChecker = fn(user: &Cred, exec: &mut ExecSettings) -> PluginResult; pub type ComplexCommandParser = fn(command: &serde_json::Value) -> Result, Box>; macro_rules! plugin_subscribe { ($plugin:ident, $plugin_type:ident, $plugin_function:ident) => { let mut api = API.lock().unwrap(); api.$plugin.push($plugin_function); }; } // Define a struct to hold the plugins pub struct PluginManager { #[cfg(feature = "finder")] role_matcher_plugins: Vec, #[cfg(feature = "finder")] task_matcher_plugins: Vec, #[cfg(feature = "finder")] user_matcher_plugins: Vec, #[cfg(feature = "finder")] duty_separation_plugins: Vec, #[cfg(feature = "finder")] task_separation_plugins: Vec, caps_filter_plugins: Vec, #[cfg(feature = "finder")] execution_checker_plugins: Vec, complex_command_parsers: Vec, } impl Default for PluginManager { fn default() -> Self { Self::new() } } impl PluginManager { pub fn new() -> Self { PluginManager { #[cfg(feature = "finder")] role_matcher_plugins: Vec::new(), #[cfg(feature = "finder")] task_matcher_plugins: Vec::new(), #[cfg(feature = "finder")] user_matcher_plugins: Vec::new(), #[cfg(feature = "finder")] duty_separation_plugins: Vec::new(), #[cfg(feature = "finder")] task_separation_plugins: Vec::new(), caps_filter_plugins: Vec::new(), #[cfg(feature = "finder")] execution_checker_plugins: Vec::new(), complex_command_parsers: Vec::new(), } } #[cfg(feature = "finder")] pub fn subscribe_role_matcher(plugin: RoleMatcher) { plugin_subscribe!(role_matcher_plugins, RoleMatcher, plugin); } #[cfg(feature = "finder")] pub fn subscribe_task_matcher(plugin: TaskMatcher) { plugin_subscribe!(task_matcher_plugins, TaskMatcher, plugin); } #[cfg(feature = "finder")] pub fn subscribe_user_matcher(plugin: UserMatcher) { plugin_subscribe!(user_matcher_plugins, UserMatcher, plugin); } #[cfg(feature = "finder")] pub fn subscribe_duty_separation(plugin: DutySeparation) { plugin_subscribe!(duty_separation_plugins, DutySeparation, plugin); } #[cfg(feature = "finder")] pub fn subscribe_task_separation(plugin: TaskSeparation) { plugin_subscribe!(task_separation_plugins, TaskSeparation, plugin); } pub fn subscribe_caps_filter(plugin: CapsFilter) { plugin_subscribe!(caps_filter_plugins, CapsFilter, plugin); } #[cfg(feature = "finder")] pub fn subscribe_privilege_checker(plugin: ExecutionChecker) { plugin_subscribe!(execution_checker_plugins, ExecutionChecker, plugin); } pub fn subscribe_complex_command_parser(plugin: ComplexCommandParser) { plugin_subscribe!(complex_command_parsers, ComplexCommandParser, plugin); } #[cfg(feature = "finder")] pub fn notify_role_matcher( role: &SRole, user: &Cred, filter: &Option, command: &[String], matcher: &mut TaskMatch, ) -> PluginResultAction { debug!("Notifying role matchers"); let api = API.lock().unwrap(); let mut result = PluginResultAction::Ignore; for plugin in api.role_matcher_plugins.iter() { debug!("Calling role matcher plugin"); match plugin(role, user, filter, command, matcher) { PluginResultAction::Override => return PluginResultAction::Override, PluginResultAction::Edit => result = PluginResultAction::Edit, PluginResultAction::Ignore => continue, } } result } #[cfg(feature = "finder")] pub fn notify_task_matcher( task: &STask, user: &Cred, command: &[String], matcher: &mut TaskMatch, ) -> PluginResultAction { let api = API.lock().unwrap(); for plugin in api.task_matcher_plugins.iter() { match plugin(task, user, command, matcher) { PluginResultAction::Override => return PluginResultAction::Override, PluginResultAction::Edit => continue, PluginResultAction::Ignore => continue, } } PluginResultAction::Ignore } #[cfg(feature = "finder")] pub fn notify_user_matcher(role: &SRole, user: &Cred, user_struct: &Value) -> ActorMatchMin { let api = API.lock().unwrap(); for plugin in api.user_matcher_plugins.iter() { let res = plugin(role, user, user_struct); if !res.is_no_match() { return res; } } ActorMatchMin::NoMatch } #[cfg(feature = "finder")] pub fn notify_duty_separation(role: &SRole, actor: &Cred) -> PluginResult { let api = API.lock().unwrap(); for plugin in api.duty_separation_plugins.iter() { match plugin(role, actor) { PluginResult::Deny => return PluginResult::Deny, PluginResult::Neutral => continue, } } PluginResult::Neutral } #[cfg(feature = "finder")] pub fn notify_task_separation(task: &STask, actor: &Cred) -> PluginResult { let api = API.lock().unwrap(); for plugin in api.task_separation_plugins.iter() { match plugin(task, actor) { PluginResult::Deny => return PluginResult::Deny, PluginResult::Neutral => continue, } } PluginResult::Neutral } pub fn notify_caps_filter(task: &STask, capabilities: &mut CapSet) -> PluginResultAction { let api = API.lock().unwrap(); for plugin in api.caps_filter_plugins.iter() { match plugin(task, capabilities) { PluginResultAction::Override => return PluginResultAction::Override, PluginResultAction::Edit => continue, PluginResultAction::Ignore => continue, } } PluginResultAction::Ignore } #[cfg(feature = "finder")] pub fn notify_privilege_checker(user: &Cred, exec: &mut ExecSettings) -> PluginResult { let api = API.lock().unwrap(); for plugin in api.execution_checker_plugins.iter() { match plugin(user, exec) { PluginResult::Deny => return PluginResult::Deny, PluginResult::Neutral => continue, } } PluginResult::Neutral } pub fn notify_complex_command_parser( command: &serde_json::Value, ) -> Result, Box> { let api = API.lock().unwrap(); for plugin in api.complex_command_parsers.iter() { match plugin(command) { Ok(result) => return Ok(result), Err(_e) => { //debug!("Error parsing command {:?}", e); continue; } } } Err("No complex command parser found".into()) } } rootasrole-core-3.2.0/src/database/actor.rs000064400000000000000000000761341046102023000167710ustar 00000000000000use std::{ borrow::Cow, fmt::{self, Display, Formatter}, }; use bon::bon; use nix::unistd::{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}; #[derive(Serialize, Debug, EnumIs, Clone, PartialEq, Eq, strum::Display)] #[serde(untagged, rename_all = "lowercase")] pub enum SGenericActorType { Id(u32), Name(String), } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct SUserType(SGenericActorType); #[derive(Deserialize, Serialize, Debug, EnumIs, Clone, PartialEq, Eq, strum::Display)] #[serde(untagged, rename_all = "lowercase")] pub enum DGenericActorType<'a> { Id(u32), #[serde(borrow)] Name(Cow<'a, str>), } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct DUserType<'a>(#[serde(borrow)] DGenericActorType<'a>); impl SUserType { pub fn fetch_id(&self) -> Option { match &self.0 { SGenericActorType::Id(id) => Some(*id), SGenericActorType::Name(name) => match User::from_name(name) { Ok(Some(user)) => Some(user.uid.as_raw()), _ => None, }, } } pub fn fetch_user(&self) -> Option { match &self.0 { SGenericActorType::Id(id) => User::from_uid((*id).into()).ok().flatten(), SGenericActorType::Name(name) => User::from_name(name).ok().flatten(), } } pub fn fetch_eq(&self, other: &Self) -> bool { let uid = self.fetch_id(); let ouid = other.fetch_id(); match (uid, ouid) { (Some(uid), Some(ouid)) => uid == ouid, _ => false, } } } impl DUserType<'_> { pub fn fetch_id(&self) -> Option { match &self.0 { DGenericActorType::Id(id) => Some(*id), DGenericActorType::Name(name) => match User::from_name(name) { Ok(Some(user)) => Some(user.uid.as_raw()), _ => None, }, } } } impl fmt::Display for SUserType { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self.0 { SGenericActorType::Id(id) => write!(f, "{}", id), SGenericActorType::Name(name) => write!(f, "{}", name), } } } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct SGroupType(SGenericActorType); #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct DGroupType<'a>(#[serde(borrow)] DGenericActorType<'a>); impl fmt::Display for SGroupType { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self.0 { SGenericActorType::Id(id) => write!(f, "{}", id), SGenericActorType::Name(name) => write!(f, "{}", name), } } } impl SGroupType { pub fn fetch_eq(&self, other: &Self) -> bool { let uid = self.fetch_id(); let ouid = other.fetch_id(); match (uid, ouid) { (Some(uid), Some(ouid)) => uid == ouid, _ => false, } } pub(super) fn fetch_id(&self) -> Option { match &self.0 { SGenericActorType::Id(id) => Some(*id), SGenericActorType::Name(name) => match Group::from_name(name) { Ok(Some(group)) => Some(group.gid.as_raw()), _ => None, }, } } pub fn fetch_group(&self) -> Option { match &self.0 { SGenericActorType::Id(id) => Group::from_gid((*id).into()).ok().flatten(), SGenericActorType::Name(name) => Group::from_name(name).ok().flatten(), } } } impl DGroupType<'_> { pub fn fetch_id(&self) -> Option { match &self.0 { DGenericActorType::Id(id) => Some(*id), DGenericActorType::Name(name) => match Group::from_name(name) { Ok(Some(group)) => Some(group.gid.as_raw()), _ => None, }, } } } impl Display for DGroupType<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self.0 { DGenericActorType::Id(id) => write!(f, "{}", id), DGenericActorType::Name(name) => write!(f, "{}", name), } } } impl Display for DUserType<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match &self.0 { DGenericActorType::Id(id) => write!(f, "{}", id), DGenericActorType::Name(name) => write!(f, "{}", name), } } } #[derive(Serialize, PartialEq, Eq, Debug, Clone, EnumIs)] #[serde(untagged)] #[repr(u32)] pub enum SGroups { Single(SGroupType) = HARDENED_ENUM_VALUE_0, Multiple(Vec) = HARDENED_ENUM_VALUE_1, } impl Display for SGroups { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { SGroups::Single(group) => write!(f, "[{}]", group), SGroups::Multiple(groups) => { let mut result = String::new(); for group in groups { result.push_str(&format!("{}, ", group)); } result.pop(); // Remove last comma result.pop(); // Remove last space write!(f, "[{}]", result) } } } } #[derive(Serialize, PartialEq, Eq, Debug, Clone, EnumIs, strum::Display)] #[serde(untagged)] pub enum DGroups<'a> { Single(#[serde(borrow)] DGroupType<'a>), Multiple(#[serde(borrow)] Cow<'a, [DGroupType<'a>]>), } impl SGroups { pub fn len(&self) -> usize { match self { SGroups::Single(_) => 1, SGroups::Multiple(groups) => groups.len(), } } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn fetch_eq(&self, other: &Self) -> bool { match (self, other) { (SGroups::Single(group), SGroups::Single(ogroup)) => group.fetch_eq(ogroup), (SGroups::Multiple(groups), SGroups::Multiple(ogroups)) => groups .iter() .all(|group| ogroups.iter().any(|ogroup| group.fetch_eq(ogroup))), _ => false, } } } impl DGroups<'_> { pub fn len(&self) -> usize { match self { DGroups::Single(_) => 1, DGroups::Multiple(groups) => groups.len(), } } pub fn is_empty(&self) -> bool { self.len() == 0 } } impl<'de: 'a, 'a> Deserialize<'de> for DGroups<'a> { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct DGroupsVisitor<'a> { marker: std::marker::PhantomData<&'a ()>, } impl<'de: 'a, 'a> serde::de::Visitor<'de> for DGroupsVisitor<'a> { type Value = DGroups<'a>; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string or a number") } fn visit_borrowed_str(self, v: &'de str) -> Result where E: serde::de::Error, { if let Ok(group) = v.parse() { Ok(DGroups::Single(DGroupType(DGenericActorType::Id(group)))) } else { Ok(DGroups::Single(DGroupType(DGenericActorType::Name( Cow::Borrowed(v), )))) } } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { if let Ok(group) = v.parse() { Ok(DGroups::Single(DGroupType(DGenericActorType::Id(group)))) } else { Ok(DGroups::Single(DGroupType(DGenericActorType::Name( Cow::Owned(v.to_string()), )))) } } fn visit_string(self, v: String) -> Result where E: serde::de::Error, { if let Ok(group) = v.parse() { Ok(DGroups::Single(DGroupType(DGenericActorType::Id(group)))) } else { Ok(DGroups::Single(DGroupType(DGenericActorType::Name( v.into(), )))) } } fn visit_u64(self, value: u64) -> Result where E: serde::de::Error, { if value > u32::MAX as u64 { return Err(E::custom("value is too large")); } Ok(DGroups::Single(DGroupType(DGenericActorType::Id( value as u32, )))) } fn visit_seq(self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { let mut groups = Vec::new(); while let Some(group) = seq.next_element::>()? { groups.push(group); } if groups.len() == 1 { Ok(DGroups::Single(groups.remove(0))) } else { Ok(DGroups::Multiple(Cow::Owned(groups))) } } } deserializer.deserialize_any(DGroupsVisitor { marker: std::marker::PhantomData, }) } } impl<'de: 'a, 'a> Deserialize<'de> for SGroups { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { struct SGroupsVisitor; impl<'de: 'a, 'a> serde::de::Visitor<'de> for SGroupsVisitor { type Value = SGroups; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string or a number") } fn visit_borrowed_str(self, v: &'de str) -> Result where E: serde::de::Error, { if let Ok(group) = v.parse() { Ok(SGroups::Single(SGroupType(SGenericActorType::Id(group)))) } else { Ok(SGroups::Single(SGroupType(SGenericActorType::Name( v.to_string(), )))) } } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { if let Ok(group) = v.parse() { Ok(SGroups::Single(SGroupType(SGenericActorType::Id(group)))) } else { Ok(SGroups::Single(SGroupType(SGenericActorType::Name( v.into(), )))) } } fn visit_string(self, v: String) -> Result where E: serde::de::Error, { if let Ok(group) = v.parse() { Ok(SGroups::Single(SGroupType(SGenericActorType::Id(group)))) } else { Ok(SGroups::Single(SGroupType(SGenericActorType::Name( v, )))) } } fn visit_u64(self, value: u64) -> Result where E: serde::de::Error, { if value > u32::MAX as u64 { return Err(E::custom("value is too large")); } Ok(SGroups::Single(SGroupType(SGenericActorType::Id( value as u32, )))) } fn visit_seq(self, mut seq: A) -> Result where A: serde::de::SeqAccess<'de>, { let mut groups = Vec::new(); while let Some(group) = seq.next_element::()? { groups.push(group); } if groups.len() == 1 { Ok(SGroups::Single(groups.remove(0))) } else { Ok(SGroups::Multiple(groups)) } } } deserializer.deserialize_any(SGroupsVisitor) } } impl From for SUserType { fn from(id: u32) -> Self { SUserType(id.into()) } } impl From for SGroupType { fn from(id: u32) -> Self { SGroupType(id.into()) } } impl From<&str> for SUserType { fn from(name: &str) -> Self { SUserType(name.into()) } } impl<'a> From<&'a str> for DUserType<'a> { fn from(name: &'a str) -> Self { DUserType(name.into()) } } impl From for DUserType<'_> { fn from(name: String) -> Self { DUserType(DGenericActorType::Name(name.into())) } } impl<'a> From<&'a str> for DGroupType<'a> { fn from(name: &'a str) -> Self { DGroupType(name.into()) } } impl From for DUserType<'_> { fn from(id: u32) -> Self { DUserType(id.into()) } } impl From for DGroupType<'_> { fn from(id: u32) -> Self { DGroupType(id.into()) } } impl From<&str> for SGroupType { fn from(name: &str) -> Self { SGroupType(name.into()) } } impl<'a> From> for DGroupType<'a> { fn from(name: Cow<'a, str>) -> Self { DGroupType(DGenericActorType::Name(name)) } } impl From for SGroupType { fn from(group: Group) -> Self { SGroupType(SGenericActorType::Id(group.gid.as_raw())) } } impl From<&str> for SGenericActorType { fn from(name: &str) -> Self { SGenericActorType::Name(name.into()) } } impl<'a> From<&'a str> for DGenericActorType<'a> { fn from(name: &'a str) -> Self { if name.parse::().is_ok() { DGenericActorType::Id(name.parse().unwrap()) } else { DGenericActorType::Name(Cow::Borrowed(name)) } } } impl From for DGenericActorType<'_> { fn from(name: u32) -> Self { DGenericActorType::Id(name) } } impl From for SGenericActorType { fn from(id: u32) -> Self { SGenericActorType::Id(id) } } impl PartialEq for SUserType { fn eq(&self, other: &User) -> bool { let uid = self.fetch_id(); match uid { Some(uid) => uid == other.uid.as_raw(), None => false, } } } impl PartialEq for DUserType<'_> { fn eq(&self, other: &User) -> bool { let uid = self.fetch_id(); match uid { Some(uid) => uid == other.uid.as_raw(), None => false, } } } impl PartialEq for SUserType { fn eq(&self, other: &str) -> bool { self.eq(&SUserType::from(other)) } } impl PartialEq for SGroupType { fn eq(&self, other: &str) -> bool { self.eq(&SGroupType::from(other)) } } impl PartialEq for SUserType { fn eq(&self, other: &u32) -> bool { self.eq(&SUserType::from(*other)) } } impl PartialEq for DUserType<'_> { fn eq(&self, other: &u32) -> bool { self.eq(&DUserType::from(*other)) } } impl PartialEq for SGroupType { fn eq(&self, other: &u32) -> bool { self.eq(&SGroupType::from(*other)) } } impl PartialEq for SGroupType { fn eq(&self, other: &Group) -> bool { let gid = self.fetch_id(); match gid { Some(gid) => gid == other.gid.as_raw(), None => false, } } } impl PartialEq for DGroupType<'_> { fn eq(&self, other: &Group) -> bool { let gid = self.fetch_id(); match gid { Some(gid) => gid == other.gid.as_raw(), None => false, } } } impl From<[SGroupType; N]> for SGroups { fn from(groups: [SGroupType; N]) -> Self { if N == 1 { SGroups::Single(groups[0].to_owned()) } else { SGroups::Multiple(groups.iter().map(|x| x.to_owned()).collect()) } } } impl TryInto> for &DGroups<'_> { type Error = String; fn try_into(self) -> Result, Self::Error> { match self { DGroups::Single(group) => Ok(vec![group .fetch_id() .ok_or(format!("{} group does not exist", group))?]), DGroups::Multiple(groups) => { let mut ids = Vec::new(); for group in groups.iter() { ids.push( group .fetch_id() .ok_or(format!("{} group does not exist", group))?, ); } Ok(ids) } } } } impl TryInto> for SGroups { type Error = String; fn try_into(self) -> Result, Self::Error> { match self { SGroups::Single(group) => Ok(vec![group .fetch_id() .ok_or(format!("{} group does not exist", group))?]), SGroups::Multiple(groups) => { let mut ids = Vec::new(); for group in groups { ids.push( group .fetch_id() .ok_or(format!("{} group does not exist", group))?, ); } Ok(ids) } } } } impl From<[&str; N]> for SGroups { fn from(groups: [&str; N]) -> Self { if N == 1 { SGroups::Single(groups[0].into()) } else { SGroups::Multiple(groups.iter().map(|&x| x.into()).collect()) } } } impl From> for SGroups { fn from(groups: Vec) -> Self { if groups.len() == 1 { SGroups::Single(groups[0].into()) } else { SGroups::Multiple(groups.into_iter().map(|x| x.into()).collect()) } } } impl From> for SGroups { fn from(groups: Vec) -> Self { if groups.len() == 1 { SGroups::Single(groups[0].clone()) } else { SGroups::Multiple(groups) } } } impl<'a> From>> for DGroups<'a> { fn from(groups: Vec>) -> Self { if groups.len() == 1 { DGroups::Single(groups[0].clone()) } else { DGroups::Multiple(Cow::Owned(groups)) } } } impl<'a> From> for DGroups<'a> { fn from(groups: DGroupType<'a>) -> Self { DGroups::Single(groups) } } impl From for SGroups { fn from(group: u32) -> Self { SGroups::Single(group.into()) } } impl From<&str> for SGroups { fn from(group: &str) -> Self { SGroups::Single(group.into()) } } impl PartialEq> for SGroups { fn eq(&self, other: &Vec) -> bool { match self { SGroups::Single(actor) => { if other.len() == 1 { return actor == &other[0]; } } SGroups::Multiple(actors) => { if actors.len() == other.len() { return actors.iter().all(|actor| other.iter().any(|x| actor == x)); } } } false } } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs)] #[serde(tag = "type", rename_all = "lowercase")] pub enum SActor { #[serde(rename = "user")] User { #[serde(alias = "name", skip_serializing_if = "Option::is_none")] id: Option, #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] _extra_fields: Map, }, #[serde(rename = "group")] Group { #[serde( alias = "names", alias = "name", skip_serializing_if = "Option::is_none" )] groups: Option, #[serde(default, flatten)] _extra_fields: Map, }, #[serde(untagged)] Unknown(Value), } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, strum::Display)] #[serde(tag = "type", rename_all = "lowercase")] pub enum DActor<'a> { #[serde(rename = "user")] #[strum(to_string = "User {id}")] User { #[serde(borrow, alias = "name")] id: DUserType<'a>, }, #[serde(rename = "group")] #[strum(to_string = "Group {groups}")] Group { #[serde(borrow, alias = "names", alias = "name", alias = "id")] groups: DGroups<'a>, }, #[serde(untagged)] Unknown(Value), } #[bon] impl SActor { #[builder(finish_fn = build)] pub fn user( #[builder(start_fn, into)] id: SUserType, #[builder(default, with = <_>::from_iter)] _extra_fields: Map, ) -> Self { SActor::User { id: Some(id), _extra_fields, } } #[builder(finish_fn = build)] pub fn group( #[builder(start_fn, into)] groups: SGroups, #[builder(default, with = <_>::from_iter)] _extra_fields: Map, ) -> Self { SActor::Group { groups: Some(groups), _extra_fields, } } } impl core::fmt::Display for SActor { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { SActor::User { id, _extra_fields } => { write!(f, "User: {}", id.as_ref().unwrap()) } SActor::Group { groups, _extra_fields, } => { write!(f, "Group: {}", groups.as_ref().unwrap()) } SActor::Unknown(unknown) => { write!(f, "Unknown: {}", unknown) } } } } #[cfg(test)] mod tests { use nix::unistd::getuid; use super::*; #[test] fn test_suser_type_creation() { let user_by_id = SUserType::from(0); let user_by_name = SUserType::from("testuser"); assert_eq!(user_by_id.to_string(), "0"); assert_eq!(user_by_name.to_string(), "testuser"); } #[test] fn test_fetch_id() { let user = SUserType::from(0); assert_eq!(user.fetch_id(), Some(0)); let group = SGroupType::from(0); assert_eq!(group.fetch_id(), Some(0)); let user = SUserType::from("root"); assert_eq!(user.fetch_id(), Some(0)); let group = SGroupType::from("root"); assert_eq!(group.fetch_id(), Some(0)); let group = SGroupType::from("unkown"); assert_eq!(group.fetch_id(), None); } #[test] fn test_fetch_user() { let user = SUserType::from("testuser"); assert!(user.fetch_user().is_none()); let user_by_id = SUserType::from(0); assert!(user_by_id.fetch_user().is_some()); } #[test] fn test_sgroups_multiple() { let groups = SGroups::from(0); assert_eq!(groups.len(), 1); let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(200)]); assert_eq!(groups.len(), 2); assert!(!groups.is_empty()); if let SGroups::Multiple(group_list) = groups { assert_eq!(group_list[0].to_string(), "0"); assert_eq!(group_list[1].to_string(), "200"); } else { panic!("Expected SGroups::Multiple"); } } #[test] fn test_fech_group() { let group = SGroupType::from(0); assert_eq!( group.fetch_group(), Some(Group::from_gid(0.into()).unwrap().unwrap()) ); let group = SGroupType::from("root"); assert_eq!( group.fetch_group(), Some(Group::from_name("root").unwrap().unwrap()) ); } #[test] fn test_is_empty() { let groups = SGroups::Multiple(vec![]); assert!(groups.is_empty()); } #[test] fn test_fetch_eq_sgroupstype_false() { let group1 = SGroupType::from("unkown"); let group2 = SGroupType::from("unkown2"); assert!(!group1.fetch_eq(&group2)); } #[test] fn test_duser_type_creation() { let user_by_id = DUserType::from(0); let user_by_name = DUserType::from("testuser"); assert_eq!(user_by_id.to_string(), "0"); assert_eq!(user_by_name.to_string(), "testuser"); } #[test] fn test_fetch_did() { let user = DUserType::from(0); assert_eq!(user.fetch_id(), Some(0)); let group = DGroupType::from(0); assert_eq!(group.fetch_id(), Some(0)); let user = DUserType::from("root"); assert_eq!(user.fetch_id(), Some(0)); let group = DGroupType::from("root"); assert_eq!(group.fetch_id(), Some(0)); let group = DGroupType::from("unkown"); assert_eq!(group.fetch_id(), None); } #[test] fn test_dgroups_single() { let groups = DGroups::from(DGroupType::from(0)); assert_eq!(groups.len(), 1); assert!(!groups.is_empty()); if let DGroups::Single(group_list) = groups { assert_eq!(group_list.to_string(), "0"); } else { panic!("Expected SGroups::Single"); } } #[test] fn test_is_dempty() { let groups = DGroups::Multiple(Cow::Borrowed(&[])); assert!(groups.is_empty()); } #[test] fn test_sactor_display() { let user = SActor::User { id: Some(SUserType::from(0)), _extra_fields: Map::new(), }; let group = SActor::Group { groups: Some(SGroups::from(vec![SGroupType::from(0)])), _extra_fields: Map::new(), }; assert_eq!(user.to_string(), "User: 0"); assert_eq!(group.to_string(), "Group: [0]"); let group = SActor::Group { groups: Some(SGroups::from(vec![ SGroupType::from(0), SGroupType::from("test"), ])), _extra_fields: Map::new(), }; assert_eq!(group.to_string(), "Group: [0, test]"); let unknown = SActor::Unknown(Value::String("unknown".to_string())); assert_eq!(unknown.to_string(), "Unknown: \"unknown\""); } #[test] fn test_display_dgrouptype() { let group = DGroupType::from("test"); assert_eq!(group.to_string(), "test"); } #[test] fn test_partialeq_sgroups() { let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from("test")]); let other_groups = vec![SGroupType::from(0), SGroupType::from("test")]; assert_eq!(groups, other_groups); let other_groups = vec![SGroupType::from(0), SGroupType::from("test2")]; assert_ne!(groups, other_groups); let other_groups = vec![SGroupType::from(0)]; assert_ne!(groups, other_groups); let other_groups = vec![ SGroupType::from(0), SGroupType::from("test"), SGroupType::from("test2"), ]; assert_ne!(groups, other_groups); let groups = SGroups::from(0); let other_groups = vec![SGroupType::from(0)]; assert_eq!(groups, other_groups); let other_groups = vec![SGroupType::from(0), SGroupType::from("test")]; assert_ne!(groups, other_groups); } #[test] fn test_sfetcheq_group() { let group1 = SGroupType::from(0); let group2 = SGroupType::from(0); assert!(group1.fetch_eq(&group2)); let group2 = SGroupType::from("root"); assert!(group1.fetch_eq(&group2)); let group2 = SGroupType::from("unkown"); assert!(!group1.fetch_eq(&group2)); let groups = SGroups::from(vec![ SGroupType::from(0), SGroupType::from(getuid().as_raw() + 1), ]); let other_groups = SGroups::from(vec![ SGroupType::from(0), SGroupType::from(getuid().as_raw() + 1), ]); assert!(groups.fetch_eq(&other_groups)); let other_groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from("test2")]); assert!(!groups.fetch_eq(&other_groups)); let other_groups = SGroups::from(0); assert!(!groups.fetch_eq(&other_groups)); let groups = SGroups::from(0); assert!(groups.fetch_eq(&other_groups)); } #[test] fn test_sfetcheq_user() { let user1 = SUserType::from(0); let user2 = SUserType::from(0); assert!(user1.fetch_eq(&user2)); let user2 = SUserType::from("root"); assert!(user1.fetch_eq(&user2)); let user2 = SUserType::from("unkown"); assert!(!user1.fetch_eq(&user2)); } #[test] fn test_from() { let cow = Cow::Borrowed("test"); let group = DGroupType::from(cow); assert_eq!(group.to_string(), "test"); let group = Group::from_gid(0.into()).unwrap().unwrap(); let group = SGroupType::from(group); assert_eq!(group.fetch_id(), Some(0)); let group = SGroups::from([SGroupType::from(0)]); assert!(group.is_single()); let group = SGroups::from(["test"]); assert!(group.is_single()); let group = SGroups::from(["test", "test2"]); assert!(!group.is_single()); let group = SGroups::from(vec![0, 1]); assert!(!group.is_single()); let group = SGroups::from(vec![0]); assert!(group.is_single()); } #[test] fn test_partialeq_user() { assert!(SUserType::from(0) == 0); assert!(SUserType::from(0) != 1); assert!(DUserType::from(0) == 0); assert!(DUserType::from(0) != 1); let user = User::from_uid(0.into()).unwrap().unwrap(); assert!(SUserType::from(0) == user); assert!(SUserType::from(0) != 1); assert!(DUserType::from(0) == user); assert!(DUserType::from(0) != 1); assert!(SUserType::from("root") == user); assert!(SUserType::from("test") != user); assert!(DUserType::from("root") == user); assert!(DUserType::from("test") != user); } #[test] fn test_partialeq_group() { let group = Group::from_gid(0.into()).unwrap().unwrap(); assert!(SGroupType::from(0) == group); assert!(SGroupType::from(1) != group); assert!(SGroupType::from("root") == group); assert!(SGroupType::from("test") != group); assert!(DGroupType::from(0) == group); assert!(DGroupType::from(1) != group); assert!(DGroupType::from("root") == group); assert!(DGroupType::from("test") != group); } #[test] fn test_tryinto_sgroups() { let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(1)]); let ids: Vec = groups.try_into().unwrap(); assert_eq!(ids, vec![0, 1]); let groups = SGroups::from(vec![SGroupType::from(0)]); let ids: Vec = groups.try_into().unwrap(); assert_eq!(ids, vec![0]); let groups = SGroups::from(vec![SGroupType::from("unkown")]); let ids: Result, _> = groups.try_into(); assert!(ids.is_err()); } #[test] fn test_tryinto_dgroups() { let groups: DGroups<'_> = DGroups::from(vec![0.into(), 1.into()]); let ids: Vec = (&groups).try_into().unwrap(); assert_eq!(ids, vec![0, 1]); let groups = DGroups::from(vec![DGroupType::from(0)]); let ids: Vec = (&groups).try_into().unwrap(); assert_eq!(ids, vec![0]); let groups = DGroups::from(vec![DGroupType::from("unkown")]); let ids: Result, _> = (&groups).try_into(); assert!(ids.is_err()); } } rootasrole-core-3.2.0/src/database/de.rs000064400000000000000000000357501046102023000162500ustar 00000000000000use core::fmt; use std::str::FromStr; use serde::Deserialize; use crate::database::structs::{SCommand, SetBehavior}; use super::{ actor::SGenericActorType, structs::{SCapabilities, SCommands}, }; use capctl::CapSet; use serde::{ de::{self, MapAccess, SeqAccess, Visitor}, Deserializer, }; use serde_json::Map; use strum::Display; impl<'de> Deserialize<'de> for SetBehavior { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct SetBehaviorVisitor; impl Visitor<'_> for SetBehaviorVisitor { type Value = SetBehavior; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string or a number") } fn visit_str(self, value: &str) -> Result where E: de::Error, { value.parse().map_err(de::Error::custom) } fn visit_i32(self, v: i32) -> Result where E: de::Error, { SetBehavior::from_repr(v as u32).ok_or(de::Error::custom(format!( "Invalid value for SetBehavior: {}", v ))) } fn visit_u8(self, v: u8) -> Result where E: de::Error, { self.visit_i32(v as i32) } fn visit_u16(self, v: u16) -> Result where E: de::Error, { self.visit_i32(v as i32) } fn visit_u32(self, v: u32) -> Result where E: de::Error, { self.visit_i32(v as i32) } fn visit_u64(self, v: u64) -> Result where E: de::Error, { if v > i32::MAX as u64 { return Err(de::Error::custom(format!( "Invalid value for SetBehavior: {}", v ))); } self.visit_i32(v as i32) } fn visit_i8(self, v: i8) -> Result where E: de::Error, { self.visit_i32(v as i32) } fn visit_i16(self, v: i16) -> Result where E: de::Error, { self.visit_i32(v as i32) } fn visit_i64(self, v: i64) -> Result where E: de::Error, { if v > i32::MAX as i64 { return Err(de::Error::custom(format!( "Invalid value for SetBehavior: {}", v ))); } self.visit_i32(v as i32) } } deserializer.deserialize_any(SetBehaviorVisitor) } } impl<'de> Deserialize<'de> for SCapabilities { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct SCapabilitiesVisitor; #[derive(Deserialize, Display)] #[serde(rename_all = "kebab-case")] #[repr(u8)] enum Field { #[serde(alias = "d")] Default, #[serde(alias = "a")] Add, #[serde(alias = "del", alias = "s")] Sub, #[serde(untagged)] Other(String), } impl<'de> Visitor<'de> for SCapabilitiesVisitor { type Value = SCapabilities; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("an array of strings or a map with SCapabilities fields") } fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { let mut add = CapSet::default(); while let Some(cap) = seq.next_element::()? { add.add(cap.parse().map_err(de::Error::custom)?); } Ok(SCapabilities { default_behavior: SetBehavior::None, add, sub: CapSet::default(), }) } fn visit_map(self, mut map: A) -> Result where A: MapAccess<'de>, { let mut default_behavior = SetBehavior::None; let mut add = CapSet::default(); let mut sub = CapSet::default(); let mut _extra_fields = Map::new(); while let Some(key) = map.next_key()? { match key { Field::Default => { default_behavior = map .next_value() .expect("default entry must be either 'all' or 'none'"); } Field::Add => { let values: Vec = map.next_value().expect("add entry must be a list"); for value in values { add.add(value.parse().map_err(|_| { de::Error::custom(format!("Invalid capability: {}", value)) })?); } } Field::Sub => { let values: Vec = map.next_value().expect("sub entry must be a list"); for value in values { sub.add(value.parse().map_err(|_| { de::Error::custom(format!("Invalid capability: {}", value)) })?); } } Field::Other(other) => { _extra_fields.insert(other.to_string(), map.next_value()?); } } } Ok(SCapabilities { default_behavior, add, sub, }) } } deserializer.deserialize_any(SCapabilitiesVisitor) } } impl<'de> Deserialize<'de> for SGenericActorType { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let raw: serde_json::Value = Deserialize::deserialize(deserializer)?; match raw { serde_json::Value::Number(num) if num.is_u64() => { Ok(SGenericActorType::Id(num.as_u64().unwrap() as u32)) } serde_json::Value::String(ref s) => { if let Ok(num) = s.parse() { Ok(SGenericActorType::Id(num)) } else { Ok(SGenericActorType::Name(s.clone())) } } _ => Err(serde::de::Error::custom( "Invalid input for SGenericActorType", )), } } } impl<'de> Deserialize<'de> for SCommands { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize, Display)] #[serde(field_identifier, rename_all = "kebab-case")] enum Fields { #[serde(alias = "d")] Default, #[serde(alias = "a")] Add, #[serde(alias = "del", alias = "s")] Sub, #[serde(untagged)] Other(String), } struct SCommandsVisitor; impl<'de> Visitor<'de> for SCommandsVisitor { type Value = SCommands; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string or a number") } fn visit_str(self, v: &str) -> Result where E: de::Error, { let set = SetBehavior::from_str(v).map_err(de::Error::custom)?; Ok(SCommands { default_behavior: Some(set), add: Vec::new(), sub: Vec::new(), _extra_fields: Map::new(), }) } fn visit_string(self, v: String) -> Result where E: de::Error, { let set = SetBehavior::from_str(&v).map_err(de::Error::custom)?; Ok(SCommands { default_behavior: Some(set), add: Vec::new(), sub: Vec::new(), _extra_fields: Map::new(), }) } fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { let mut add = Vec::new(); while let Some(cmd) = seq.next_element::()? { add.push(cmd); } Ok(SCommands { default_behavior: Some(SetBehavior::None), add, sub: Vec::new(), _extra_fields: Map::new(), }) } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'de>, { let mut default_behavior = None; let mut add = Vec::new(); let mut sub = Vec::new(); let mut _extra_fields = Map::new(); while let Some(key) = map.next_key()? { match key { Fields::Default => { default_behavior = Some( map.next_value() .expect("default entry must be either 'all' or 'none'"), ); } Fields::Add => { let values: Vec = map.next_value().expect("add entry must be a list"); add.extend(values); } Fields::Sub => { let values: Vec = map.next_value().expect("sub entry must be a list"); sub.extend(values); } Fields::Other(other) => { _extra_fields.insert(other.to_string(), map.next_value()?); } } } Ok(SCommands { default_behavior, add, sub, _extra_fields, }) } } deserializer.deserialize_any(SCommandsVisitor) } } #[cfg(test)] mod tests { use crate::util::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2}; use super::*; use capctl::Cap; use serde_json::json; #[test] fn test_set_behavior_deserialization() { let json_data = json!("none"); let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); assert_eq!(behavior, SetBehavior::None); let json_data = json!("all"); let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); assert_eq!(behavior, SetBehavior::All); let json_data = json!(HARDENED_ENUM_VALUE_0); let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); assert_eq!( behavior, SetBehavior::from_repr(HARDENED_ENUM_VALUE_0).unwrap() ); let json_data = json!(HARDENED_ENUM_VALUE_1); let behavior: SetBehavior = serde_json::from_value(json_data).unwrap(); assert_eq!( behavior, SetBehavior::from_repr(HARDENED_ENUM_VALUE_1).unwrap() ); let invalid_data = json!(HARDENED_ENUM_VALUE_2); assert!(serde_json::from_value::(invalid_data).is_err()); } #[test] fn test_s_capabilities_deserialization_seq() { let json_data = json!(["CAP_SYS_ADMIN", "CAP_NET_BIND_SERVICE", "CAP_CHOWN"]); let caps: SCapabilities = serde_json::from_value(json_data).unwrap(); assert!(caps.add.has(Cap::SYS_ADMIN)); assert!(caps.add.has(Cap::NET_BIND_SERVICE)); assert!(caps.add.has(Cap::CHOWN)); assert_eq!(caps.default_behavior, SetBehavior::None); } #[test] fn test_s_capabilities_deserialization_map() { let json_data = json!({ "default": "none", "add": ["CAP_SYS_ADMIN", "CAP_CHOWN"], "sub": ["CAP_NET_RAW"] }); let caps: SCapabilities = serde_json::from_value(json_data).unwrap(); assert!(caps.add.has(Cap::SYS_ADMIN)); assert!(caps.add.has(Cap::CHOWN)); assert!(caps.sub.has(Cap::NET_RAW)); assert_eq!(caps.default_behavior, SetBehavior::None); } #[test] fn test_invalid_capabilities() { let invalid_data = json!(["INVALID_CAPABILITY", "CAP_FAKE"]); assert!(serde_json::from_value::(invalid_data).is_err()); } #[test] fn test_s_generic_actor_type_deserialization() { let json_data = json!(42); let actor_type: SGenericActorType = serde_json::from_value(json_data).unwrap(); assert_eq!(actor_type, SGenericActorType::Id(42)); let json_data = json!("actor_name"); let actor_type: SGenericActorType = serde_json::from_value(json_data).unwrap(); assert_eq!( actor_type, SGenericActorType::Name("actor_name".to_string()) ); let invalid_data = json!(null); assert!(serde_json::from_value::(invalid_data).is_err()); } #[test] fn test_s_commands_deserialization_seq() { let json_data = json!(["/bin/ls", "/bin/cat"]); let commands: SCommands = serde_json::from_value(json_data).unwrap(); assert_eq!(commands.add.len(), 2); assert_eq!(commands.add[0], "/bin/ls".into()); assert_eq!(commands.add[1], "/bin/cat".into()); } #[test] fn test_s_commands_deserialization_map() { let json_data = json!({ "default": "all", "add": ["/bin/ls"], "sub": ["/bin/cat"] }); let commands: SCommands = serde_json::from_value(json_data).unwrap(); assert_eq!(commands.default_behavior.unwrap(), SetBehavior::All); assert_eq!(commands.add.len(), 1); assert_eq!(commands.add[0], "/bin/ls".into()); assert_eq!(commands.sub.len(), 1); assert_eq!(commands.sub[0], "/bin/cat".into()); } } rootasrole-core-3.2.0/src/database/migration.rs000064400000000000000000000167351046102023000176530ustar 00000000000000use std::error::Error; use log::debug; use semver::Version; use crate::PACKAGE_VERSION; type MigrationFn = fn(&Migration, &mut T) -> Result<(), Box>; pub struct Migration { pub from: fn() -> Version, pub to: fn() -> Version, pub up: MigrationFn, pub down: MigrationFn, } #[derive(PartialEq, Eq, Debug)] pub enum ChangeResult { UpgradeDirect, DowngradeDirect, UpgradeIndirect, DowngradeIndirect, None, } impl Migration { pub fn from(&self) -> Version { (self.from)() } pub fn to(&self) -> Version { (self.to)() } pub fn change( &self, doc: &mut T, from: &Version, to: &Version, ) -> Result> { debug!("Checking migration from {} to {} :", self.from(), self.to()); #[cfg(not(tarpaulin_include))] debug!( " \tself.from() == *from -> {}\tself.from() == *to -> {} \tself.to() == *to -> {}\tself.to() == *from -> {} \t*from < *to -> {}\tself.to() < *to -> {}\tself.to() > *from -> {} \t*from > *to -> {}\tself.from() < *to -> {}\tself.from() > *from -> {}", self.from() == *from, self.to() == *from, self.to() == *to, self.to() == *from, *from < *to, self.to() < *to, self.to() > *from, *from > *to, self.from() < *to, self.from() > *from ); if self.from() == *from && self.to() == *to { debug!("Direct Upgrading from {} to {}", self.from(), self.to()); (self.up)(self, doc)?; Ok(ChangeResult::UpgradeDirect) } else if self.to() == *from && self.from() == *to { debug!("Direct Downgrading from {} to {}", self.to(), self.from()); (self.down)(self, doc)?; Ok(ChangeResult::DowngradeDirect) } else if *from < *to && self.from() == *from && self.to() < *to && self.to() > *from { debug!("Step Upgrading from {} to {}", self.from(), self.to()); // 1.0.0 -> 2.0.0 -> 3.0.0 (self.up)(self, doc)?; Ok(ChangeResult::UpgradeIndirect) } else if *from > *to && self.to() == *from && self.from() > *to && self.from() < *from { debug!("Step Downgrading from {} to {}", self.to(), self.from()); // 3.0.0 -> 2.0.0 -> 1.0.0 (self.down)(self, doc)?; Ok(ChangeResult::DowngradeIndirect) } else { Ok(ChangeResult::None) } } pub fn migrate_from( from: &Version, to: &Version, doc: &mut T, migrations: &[Self], ) -> Result> { let mut from = from.clone(); let to = to.clone(); debug!("===== Migrating from {} to {} =====", from, to); if from != to { let mut migrated = ChangeResult::UpgradeIndirect; while migrated == ChangeResult::UpgradeIndirect || migrated == ChangeResult::DowngradeIndirect { migrated = ChangeResult::None; for migration in migrations { match migration.change(doc, &from, &to)? { ChangeResult::UpgradeDirect | ChangeResult::DowngradeDirect => { return Ok(true); } ChangeResult::UpgradeIndirect => { from = migration.to(); migrated = ChangeResult::UpgradeIndirect; break; } ChangeResult::DowngradeIndirect => { from = migration.from(); migrated = ChangeResult::DowngradeIndirect; break; } ChangeResult::None => { migrated = ChangeResult::None; } } } if migrated == ChangeResult::None { return Err(format!("No migration from {} to {} found", from, to).into()); } } } Ok(false) } /// Migrate the database schema to the current version. /// If the version is already the current version, nothing is done. /// If the version is older, the database is upgraded. /// If the version is newer, the database is downgraded. /// Returns true if the database was migrated, false if it was already at the current version. pub fn migrate( version: &Version, doc: &mut T, migrations: &[Self], ) -> Result> where { Self::migrate_from( version, &Version::parse(PACKAGE_VERSION).unwrap(), doc, migrations, ) } } #[cfg(test)] mod tests { use super::*; use semver::Version; #[test] fn test_migration() { let mut doc = 0; let migrations = vec![ Migration { from: || Version::parse("1.0.0").unwrap(), to: || Version::parse("2.0.0").unwrap(), up: |_, doc| { *doc += 1; Ok(()) }, down: |_, doc| { *doc -= 1; Ok(()) }, }, Migration { from: || Version::parse("2.0.0").unwrap(), to: || Version::parse("3.0.0-alpha.1").unwrap(), up: |_, doc| { *doc += 1; Ok(()) }, down: |_, doc| { *doc -= 1; Ok(()) }, }, Migration { from: || Version::parse("3.0.0-alpha.1").unwrap(), to: || Version::parse(PACKAGE_VERSION).unwrap(), up: |_, doc| { *doc += 1; Ok(()) }, down: |_, doc| { *doc -= 1; Ok(()) }, }, Migration { from: || Version::parse(PACKAGE_VERSION).unwrap(), to: || Version::parse("4.0.0").unwrap(), up: |_, doc| { *doc += 1; Ok(()) }, down: |_, doc| { *doc -= 1; Ok(()) }, }, ]; assert!( Migration::migrate(&Version::parse("1.0.0").unwrap(), &mut doc, &migrations).unwrap() ); assert_eq!(doc, 3); doc = 0; assert!( Migration::migrate(&Version::parse("2.0.0").unwrap(), &mut doc, &migrations).unwrap() ); assert_eq!(doc, 2); doc = 0; assert!( Migration::migrate( &Version::parse("3.0.0-alpha.1").unwrap(), &mut doc, &migrations ) .unwrap() ); assert_eq!(doc, 1); doc = 0; assert!( Migration::migrate(&Version::parse("4.0.0").unwrap(), &mut doc, &migrations).unwrap() ); assert_eq!(doc, -1); doc = 0; assert!( !Migration::migrate( &Version::parse(PACKAGE_VERSION).unwrap(), &mut doc, &migrations ) .unwrap() ); assert_eq!(doc, 0); } } rootasrole-core-3.2.0/src/database/mod.rs000064400000000000000000000245001046102023000164260ustar 00000000000000use std::error::Error; use actor::{SGroups, SUserType}; use bon::{builder, Builder}; use chrono::Duration; use linked_hash_set::LinkedHashSet; use options::EnvBehavior; use serde::{de::Deserialize, de::Deserializer, Serialize}; use self::options::EnvKey; #[cfg(feature = "finder")] pub mod score; pub mod actor; pub mod de; pub mod migration; pub mod options; pub mod ser; pub mod structs; pub mod versionning; #[derive(Debug, Default, Builder)] #[builder(on(_, overwritable))] pub struct FilterMatcher { pub role: Option, pub task: Option, pub env_behavior: Option, #[builder(with = |s: impl Into| -> Result<_,String> { s.into().fetch_id().ok_or("This user does not exist".into()) })] pub user: Option, #[builder(with = |s: impl Into| -> Result<_,String> { s.into().try_into() })] pub group: Option>, } // deserialize the linked hash set fn lhs_deserialize_envkey<'de, D>( deserializer: D, ) -> Result>, D::Error> where D: Deserializer<'de>, { if let Ok(v) = Vec::::deserialize(deserializer) { Ok(Some(v.into_iter().collect())) } else { Ok(None) } } // serialize the linked hash set fn lhs_serialize_envkey( value: &Option>, serializer: S, ) -> Result where S: serde::Serializer, { if let Some(v) = value { let v: Vec = v.iter().cloned().collect(); v.serialize(serializer) } else { serializer.serialize_none() } } // deserialize the linked hash set fn lhs_deserialize<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { if let Ok(v) = Vec::::deserialize(deserializer) { Ok(Some(v.into_iter().collect())) } else { Ok(None) } } // serialize the linked hash set fn lhs_serialize(value: &Option>, serializer: S) -> Result where S: serde::Serializer, { if let Some(v) = value { let v: Vec = v.iter().cloned().collect(); v.serialize(serializer) } else { serializer.serialize_none() } } pub fn is_default(t: &T) -> bool { t == &T::default() } pub fn serialize_duration(value: &Option, serializer: S) -> Result where S: serde::Serializer, { // hh:mm:ss format match value { Some(value) => serializer.serialize_str(&format!( "{:#02}:{:#02}:{:#02}", value.num_hours(), value.num_minutes() % 60, value.num_seconds() % 60 )), None => serializer.serialize_none(), } } pub fn deserialize_duration<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; match convert_string_to_duration(&s) { Ok(d) => Ok(d), Err(e) => Err(serde::de::Error::custom(e)), } } fn convert_string_to_duration(s: &String) -> Result, Box> { let mut parts = s.split(':'); //unwrap or error if let (Some(hours), Some(minutes), Some(seconds)) = (parts.next(), parts.next(), parts.next()) { let hours: i64 = hours.parse()?; let minutes: i64 = minutes.parse()?; let seconds: i64 = seconds.parse()?; return Ok(Some( Duration::hours(hours) + Duration::minutes(minutes) + Duration::seconds(seconds), )); } Err("Invalid duration format".into()) } fn serialize_capset(value: &capctl::CapSet, serializer: S) -> Result where S: serde::Serializer, { let v: Vec = value.iter().map(|cap| cap.to_string()).collect(); v.serialize(serializer) } #[cfg(test)] mod tests { use super::*; struct LinkedHashSetTester(pub Option>); impl<'de> Deserialize<'de> for LinkedHashSetTester { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { Ok(Self(lhs_deserialize_envkey(deserializer)?)) } } impl Serialize for LinkedHashSetTester { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { lhs_serialize_envkey(&self.0, serializer) } } impl<'de> Deserialize<'de> for LinkedHashSetTester { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { Ok(Self(lhs_deserialize(deserializer)?)) } } impl Serialize for LinkedHashSetTester { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { lhs_serialize(&self.0, serializer) } } struct DurationTester(Option); impl<'de> Deserialize<'de> for DurationTester { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { Ok(Self(deserialize_duration(deserializer)?)) } } impl Serialize for DurationTester { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serialize_duration(&self.0, serializer) } } #[test] fn test_lhs_deserialize_envkey() { let json = r#"["key1", "key2", "key3"]"#; let deserialized: Option> = serde_json::from_str(json).unwrap(); assert!(deserialized.is_some()); let set = deserialized.unwrap().0.unwrap(); assert_eq!(set.len(), 3); assert!(set.contains(&EnvKey::from("key1"))); assert!(set.contains(&EnvKey::from("key2"))); assert!(set.contains(&EnvKey::from("key3"))); } #[test] fn test_lhs_deserialize() { let json = r#"["value1", "value2", "value3"]"#; let deserialized: Option> = serde_json::from_str(json).unwrap(); assert!(deserialized.is_some()); let set = deserialized.unwrap().0.unwrap(); assert_eq!(set.len(), 3); assert!(set.contains("value1")); assert!(set.contains("value2")); assert!(set.contains("value3")); } #[test] fn test_lhs_serialize() { let mut set = LinkedHashSetTester(Some(LinkedHashSet::new())); set.0.as_mut().unwrap().insert("value1".to_string()); set.0.as_mut().unwrap().insert("value2".to_string()); set.0.as_mut().unwrap().insert("value3".to_string()); let serialized = serde_json::to_string(&Some(set)).unwrap(); assert_eq!(serialized, r#"["value1","value2","value3"]"#); } #[test] fn test_serialize_duration() { let duration = DurationTester(Some(Duration::seconds(3661))); let serialized = serde_json::to_string(&duration).unwrap(); assert_eq!(serialized, r#""01:01:01""#); } #[test] fn test_deserialize_duration() { let json = r#""01:01:01""#; let deserialized: DurationTester = serde_json::from_str(json).unwrap(); assert!(deserialized.0.is_some()); let duration = deserialized.0.unwrap(); assert_eq!(duration.num_seconds(), 3661); } #[test] fn test_is_default() { assert!(is_default(&0)); assert!(is_default(&String::new())); assert!(!is_default(&1)); assert!(!is_default(&"non-default".to_string())); } #[test] fn test_lhs_serialize_empty() { let set: LinkedHashSetTester = LinkedHashSetTester(None); let serialized = serde_json::to_string(&Some(set)).unwrap(); assert_eq!(serialized, r#"null"#); let set: LinkedHashSetTester = LinkedHashSetTester(None); let serialized = serde_json::to_string(&Some(set)).unwrap(); assert_eq!(serialized, r#"null"#); let duration = DurationTester(None); let serialized = serde_json::to_string(&duration).unwrap(); assert_eq!(serialized, r#"null"#); } #[test] fn test_lhs_deserialize_envkey_null() { let json = r#"null"#; let deserialized: Option> = serde_json::from_str(json).unwrap(); assert!(deserialized.is_none()); } #[test] fn test_lhs_deserialize_empty_object() { let json = r#"{}"#; let deserialized: Result>, _> = serde_json::from_str(json); assert!(deserialized.is_err()); } #[test] fn test_lhs_serialize_empty_set() { let set = LinkedHashSetTester(Some(LinkedHashSet::::new())); let serialized = serde_json::to_string(&Some(set)).unwrap(); assert_eq!(serialized, r#"[]"#); } #[test] fn test_serialize_duration_large() { let duration = Some(DurationTester(Some(Duration::seconds(3600 * 25 + 61)))); let serialized = serde_json::to_string(&duration).unwrap(); assert_eq!(serialized, r#""25:01:01""#); } #[test] fn test_deserialize_duration_leading_zeros() { let json = r#""001:002:003""#; let deserialized: DurationTester = serde_json::from_str(json).unwrap(); assert!(deserialized.0.is_some()); let duration = deserialized.0.unwrap(); assert_eq!(duration.num_seconds(), 3723); } #[test] fn test_deserialize_duration_with_spaces() { let json = r#"" 01:01:01 ""#; let deserialized: Result = serde_json::from_str(json); assert!(deserialized.is_err()); } #[test] fn test_deserialize_duration_non_numeric() { let json = r#""aa:bb:cc""#; let deserialized: Result = serde_json::from_str(json); assert!(deserialized.is_err()); } #[test] fn test_deserialize_duration_invalid() { let json = r#""test""#; let deserialized: Result = serde_json::from_str(json); assert!(deserialized.is_err()); } #[test] fn test_lhs_deserialize_envkey_mixed_types() { let json = r#"["key1", 123, null]"#; let deserialized: Result, _> = serde_json::from_str(json); assert!(deserialized.is_err()); } } rootasrole-core-3.2.0/src/database/options.rs000064400000000000000000001340201046102023000173410ustar 00000000000000use std::collections::HashMap; use std::{borrow::Borrow, cell::RefCell, rc::Rc}; use std::{env, result::Result}; use bon::{bon, builder, Builder}; use chrono::Duration; use konst::eq_str; use linked_hash_set::LinkedHashSet; #[cfg(feature = "pcre2")] use pcre2::bytes::Regex; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; use strum::{Display, EnumIs, EnumIter, EnumString, FromRepr}; use log::debug; use crate::rc_refcell; use crate::util::{ HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2, HARDENED_ENUM_VALUE_3, }; use super::{ convert_string_to_duration, deserialize_duration, is_default, serialize_duration, FilterMatcher, }; use super::{ lhs_deserialize, lhs_deserialize_envkey, lhs_serialize, lhs_serialize_envkey, structs::{SConfig, SRole, STask}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] #[repr(u8)] pub enum Level { #[default] None, Default, Global, Role, Task, } #[derive(Debug, Clone, Copy, FromRepr, EnumIter, Display)] pub enum OptType { Path, Env, Root, Bounding, Timeout, } #[derive( Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, )] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] #[repr(u32)] pub enum PathBehavior { Delete = HARDENED_ENUM_VALUE_0, KeepSafe = HARDENED_ENUM_VALUE_1, KeepUnsafe = HARDENED_ENUM_VALUE_2, #[default] Inherit = HARDENED_ENUM_VALUE_3, } #[derive( Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone, Copy, Display, EnumString, )] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] #[repr(u8)] pub enum TimestampType { #[default] PPID, TTY, UID, } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)] pub struct STimeout { #[serde(default, rename = "type", skip_serializing_if = "Option::is_none")] pub type_field: Option, #[serde( serialize_with = "serialize_duration", deserialize_with = "deserialize_duration", skip_serializing_if = "Option::is_none" )] pub duration: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub max_usage: Option, #[serde(default)] #[serde(flatten, skip_serializing_if = "Map::is_empty")] #[builder(default)] pub _extra_fields: Map, } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Builder)] pub struct SPathOptions { #[serde(rename = "default", default, skip_serializing_if = "is_default")] #[builder(start_fn)] pub default_behavior: PathBehavior, #[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>, } // ...existing code... impl SPathOptions {} // ...existing code... #[derive( Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, )] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] #[repr(u32)] pub enum EnvBehavior { Delete = HARDENED_ENUM_VALUE_0, Keep = HARDENED_ENUM_VALUE_1, #[default] Inherit = HARDENED_ENUM_VALUE_2, } #[derive(Serialize, Hash, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] enum EnvKeyType { Wildcarded, Normal, } #[derive(Eq, Hash, PartialEq, Serialize, Debug, Clone, Builder)] #[serde(transparent)] pub struct EnvKey { #[serde(skip)] env_type: EnvKeyType, value: String, } impl std::fmt::Display for EnvKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.value) } } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default, Builder)] pub struct SEnvOptions { #[serde(rename = "default", default, skip_serializing_if = "is_default")] #[builder(start_fn)] pub default_behavior: EnvBehavior, #[serde(alias = "override", default, skip_serializing_if = "Option::is_none")] pub override_behavior: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[builder(with = |iter: impl IntoIterator| { let mut map = HashMap::with_hasher(Default::default()); map.extend(iter.into_iter().map(|(k, v)| (k.to_string(), v.to_string()))); map })] pub set: Option>, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "lhs_deserialize_envkey", serialize_with = "lhs_serialize_envkey" )] #[builder(with = |v : impl IntoIterator| -> Result<_,String> { let mut res = LinkedHashSet::new(); for s in v { res.insert(EnvKey::new(s.to_string())?); } Ok(res)})] pub keep: Option>, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "lhs_deserialize_envkey", serialize_with = "lhs_serialize_envkey" )] #[builder(with = |v : impl IntoIterator| -> Result<_,String> { let mut res = LinkedHashSet::new(); for s in v { res.insert(EnvKey::new(s.to_string())?); } Ok(res)})] pub check: Option>, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "lhs_deserialize_envkey", serialize_with = "lhs_serialize_envkey" )] #[builder(with = |v : impl IntoIterator| -> Result<_,String> { let mut res = LinkedHashSet::new(); for s in v { res.insert(EnvKey::new(s.to_string())?); } Ok(res)})] pub delete: Option>, #[serde(default, flatten)] #[builder(default)] pub _extra_fields: Map, } #[derive( Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, )] #[strum(ascii_case_insensitive)] #[serde(rename_all = "lowercase")] #[derive(Default)] #[repr(u32)] pub enum SBounding { Strict = HARDENED_ENUM_VALUE_0, #[default] Inherit = HARDENED_ENUM_VALUE_1, Ignore = HARDENED_ENUM_VALUE_2, } #[derive( Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, )] #[strum(ascii_case_insensitive)] #[serde(rename_all = "kebab-case")] #[derive(Default)] #[repr(u32)] pub enum SPrivileged { #[default] User = HARDENED_ENUM_VALUE_0, Inherit = HARDENED_ENUM_VALUE_1, Privileged = HARDENED_ENUM_VALUE_2, } #[derive( Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Display, Clone, Copy, EnumString, )] #[strum(ascii_case_insensitive)] #[serde(rename_all = "kebab-case")] #[derive(Default)] #[repr(u32)] pub enum SAuthentication { #[default] Perform = HARDENED_ENUM_VALUE_0, Inherit = HARDENED_ENUM_VALUE_1, Skip = HARDENED_ENUM_VALUE_2, } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct Opt { #[serde(skip)] pub level: Level, #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub env: Option, #[serde(skip_serializing_if = "Option::is_none")] pub root: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bounding: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub authentication: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout: Option, #[serde(default, flatten)] pub _extra_fields: Map, } #[bon] impl Opt { #[builder] pub fn new( #[builder(start_fn)] level: Level, path: Option, env: Option, root: Option, bounding: Option, authentication: Option, timeout: Option, #[builder(default)] _extra_fields: Map, ) -> Self { Opt { level, path, env, root, bounding, authentication, timeout, _extra_fields, } } pub fn level_default() -> Self { Self::builder(Level::Default) .maybe_root(env!("RAR_USER_CONSIDERED").parse().ok()) .maybe_bounding(env!("RAR_BOUNDING").parse().ok()) .path(SPathOptions::level_default()) .maybe_authentication(env!("RAR_AUTHENTICATION").parse().ok()) .env( SEnvOptions::builder( env!("RAR_ENV_DEFAULT") .parse() .unwrap_or(EnvBehavior::Delete), ) .keep(env!("RAR_ENV_KEEP_LIST").split(',').collect::>()) .unwrap() .check( env!("RAR_ENV_CHECK_LIST") .split(',') .filter(|s| { #[cfg(feature = "pcre2")] return is_valid_env_name(s) || is_regex(s); #[cfg(not(feature = "pcre2"))] is_valid_env_name(&s) }) .collect::>(), ) .unwrap() .delete( env!("RAR_ENV_DELETE_LIST") .split(',') .collect::>(), ) .unwrap() .set( serde_json::from_str(env!("RAR_ENV_SET_LIST")) .unwrap_or_else(|_| Map::default()), ) .maybe_override_behavior(env!("RAR_ENV_OVERRIDE_BEHAVIOR").parse().ok()) .build(), ) .timeout( STimeout::builder() .maybe_type_field(env!("RAR_TIMEOUT_TYPE").parse().ok()) .maybe_duration( convert_string_to_duration(&env!("RAR_TIMEOUT_DURATION").to_string()) .ok() .flatten(), ) .build(), ) .build() } } impl Default for Opt { fn default() -> Self { Opt { path: Some(SPathOptions::default()), env: Some(SEnvOptions::default()), root: Some(SPrivileged::default()), bounding: Some(SBounding::default()), authentication: None, timeout: None, _extra_fields: Map::default(), level: Level::Default, } } } impl Default for SPathOptions { fn default() -> Self { SPathOptions { default_behavior: PathBehavior::Inherit, add: None, sub: None, } } } impl SPathOptions { pub fn level_default() -> Self { SPathOptions::builder( env!("RAR_PATH_DEFAULT") .parse() .unwrap_or(PathBehavior::Delete), ) .add(env!("RAR_PATH_ADD_LIST").split(':').collect::>()) .sub( env!("RAR_PATH_REMOVE_LIST") .split(':') .collect::>(), ) .build() } } fn is_valid_env_name(s: &str) -> bool { let mut chars = s.chars(); // Check if the first character is a letter or underscore if let Some(first_char) = chars.next() { if !(first_char.is_ascii_alphabetic() || first_char == '_') { return false; } } else { return false; // Empty string } // Check if the remaining characters are alphanumeric or underscores chars.all(|c| c.is_ascii_alphanumeric() || c == '_') } #[cfg(feature = "pcre2")] fn is_regex(s: &str) -> bool { Regex::new(&format!("^{}$", s)).is_ok() } #[cfg(not(feature = "pcre2"))] fn is_regex(_s: &str) -> bool { false // Always return true if regex feature is disabled } impl EnvKey { pub fn new(s: String) -> Result { //debug!("Creating env key: {}", s); if is_valid_env_name(&s) { Ok(EnvKey { env_type: EnvKeyType::Normal, value: s, }) } else if is_regex(&s) { Ok(EnvKey { env_type: EnvKeyType::Wildcarded, value: s, }) } else { Err(format!( "env key {}, must be a valid env, or a valid regex", s )) } } } impl PartialEq for EnvKey { fn eq(&self, other: &str) -> bool { self.value == *other } } impl From for String { fn from(val: EnvKey) -> Self { val.value } } impl From for EnvKey { fn from(s: String) -> Self { EnvKey::new(s).expect("Invalid env key") } } impl From<&str> for EnvKey { fn from(s: &str) -> Self { EnvKey::new(s.into()).expect("Invalid env key") } } impl<'de> Deserialize<'de> for EnvKey { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; EnvKey::new(s).map_err(serde::de::Error::custom) } } trait EnvSet { fn env_matches(&self, wildcarded: &EnvKey) -> bool; } impl EnvSet for LinkedHashSet { fn env_matches(&self, needle: &EnvKey) -> bool { self.iter().any(|s| match s.env_type { EnvKeyType::Normal => s == needle, EnvKeyType::Wildcarded => check_wildcarded(s, &needle.value), }) } } impl EnvSet for Option> { fn env_matches(&self, needle: &EnvKey) -> bool { self.as_ref().is_some_and(|set| set.env_matches(needle)) } } #[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) } #[cfg(not(feature = "pcre2"))] fn check_wildcarded(_wildcarded: &EnvKey, _s: &String) -> bool { true } #[derive(Debug, PartialEq)] pub struct ConstParseError(pub &'static str); use std::fmt::{self, Display}; impl Display for ConstParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_fmt(format_args!( "Failed to parse the const {} defined in .cargo/config.toml", self.0 )) } } impl ConstParseError { const fn panic(&self) -> ! { panic!("failed to parse a const") } } impl PathBehavior { pub const fn try_parse(input: &str) -> std::result::Result { match input { _ if eq_str(input, "delete") => Ok(PathBehavior::Delete), _ if eq_str(input, "keep_safe") => Ok(PathBehavior::KeepSafe), _ if eq_str(input, "keep_unsafe") => Ok(PathBehavior::KeepUnsafe), _ if eq_str(input, "inherit") => Ok(PathBehavior::Inherit), _ => ConstParseError("PathBehavior").panic(), } } } impl EnvBehavior { pub const fn try_parse(input: &str) -> std::result::Result { match input { _ if eq_str(input, "delete") => Ok(EnvBehavior::Delete), _ if eq_str(input, "keep") => Ok(EnvBehavior::Keep), _ if eq_str(input, "inherit") => Ok(EnvBehavior::Inherit), _ => ConstParseError("EnvBehavior").panic(), } } } impl SPrivileged { pub const fn try_parse(input: &str) -> std::result::Result { match input { _ if eq_str(input, "user") => Ok(SPrivileged::User), _ if eq_str(input, "inherit") => Ok(SPrivileged::Inherit), _ if eq_str(input, "privileged") => Ok(SPrivileged::Privileged), _ => ConstParseError("SPrivileged").panic(), } } } impl TimestampType { pub const fn try_parse(input: &str) -> std::result::Result { match input { _ if eq_str(input, "ppid") => Ok(TimestampType::PPID), _ if eq_str(input, "tty") => Ok(TimestampType::TTY), _ if eq_str(input, "uid") => Ok(TimestampType::UID), _ => ConstParseError("TimestampType").panic(), } } } impl SBounding { pub const fn try_parse(input: &str) -> std::result::Result { match input { _ if eq_str(input, "strict") => Ok(SBounding::Strict), _ if eq_str(input, "inherit") => Ok(SBounding::Inherit), _ if eq_str(input, "ignore") => Ok(SBounding::Ignore), _ => ConstParseError("SBounding").panic(), } } } impl SAuthentication { pub const fn try_parse(input: &str) -> std::result::Result { match input { _ if eq_str(input, "perform") => Ok(SAuthentication::Perform), _ if eq_str(input, "inherit") => Ok(SAuthentication::Inherit), _ if eq_str(input, "skip") => Ok(SAuthentication::Skip), _ => ConstParseError("SAuthentication").panic(), } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OptStack { pub(crate) stack: [Option>>; 5], roles: Option>>, role: Option>>, task: Option>>, } #[cfg(not(tarpaulin_include))] impl OptStackBuilder { fn opt(mut self, opt: Option>>) -> Self { if let Some(opt) = opt { self.stack[opt.as_ref().borrow().level as usize] = Some(opt.clone()); } self } fn with_task( self, task: Rc>, ) -> OptStackBuilder< opt_stack_builder::SetTask>>, > where ::Roles: opt_stack_builder::IsUnset, ::Role: opt_stack_builder::IsUnset, ::Task: opt_stack_builder::IsUnset, { self.with_role( task.as_ref() .borrow() ._role .as_ref() .unwrap() .upgrade() .unwrap(), ) .task(task.to_owned()) .opt(task.as_ref().borrow().options.to_owned()) } fn with_role( self, role: Rc>, ) -> OptStackBuilder>> where ::Roles: opt_stack_builder::IsUnset, ::Role: opt_stack_builder::IsUnset, { self.with_roles( role.as_ref() .borrow() ._config .as_ref() .unwrap() .upgrade() .unwrap(), ) .role(role.to_owned()) .opt(role.as_ref().borrow().options.to_owned()) } fn with_roles( self, roles: Rc>, ) -> OptStackBuilder> where ::Roles: opt_stack_builder::IsUnset, { self.with_default() .roles(roles.to_owned()) .opt(roles.as_ref().borrow().options.to_owned()) } fn with_default(self) -> Self { self.opt(Some(rc_refcell!(Opt::level_default()))) } } #[bon] impl OptStack { #[builder] pub fn new( #[builder(field)] stack: [Option>>; 5], roles: Option>>, role: Option>>, task: Option>>, ) -> Self { OptStack { stack, roles, role, task, } } pub fn from_task(task: Rc>) -> Self { OptStack::builder().with_task(task).build() } pub fn from_role(role: Rc>) -> Self { OptStack::builder().with_role(role).build() } pub fn from_roles(roles: Rc>) -> Self { OptStack::builder().with_roles(roles).build() } fn find_in_options Option<(Level, V)>, V>(&self, f: F) -> Option<(Level, V)> { for opt in self.stack.iter().rev() { if let Some(opt) = opt.to_owned() { let res = f(&opt.as_ref().borrow()); if res.is_some() { debug!("res: {:?}", res.as_ref().unwrap().0); return res; } } } None } fn iter_in_options(&self, mut f: F) { for opt in self.stack.iter() { if let Some(opt) = opt.to_owned() { f(&opt.as_ref().borrow()); } } } fn get_final_path(&self) -> SPathOptions { let mut final_behavior = PathBehavior::Delete; let default = LinkedHashSet::new(); let final_add = rc_refcell!(LinkedHashSet::new()); // Cannot use HashSet as we need to keep order let final_sub = rc_refcell!(LinkedHashSet::new()); self.iter_in_options(|opt| { let final_add_clone = Rc::clone(&final_add); let final_sub_clone = Rc::clone(&final_sub); if let Some(p) = opt.path.borrow().as_ref() { match p.default_behavior { PathBehavior::KeepSafe | PathBehavior::KeepUnsafe | PathBehavior::Delete => { if let Some(add) = p.add.as_ref() { final_add_clone.as_ref().replace(add.clone()); } if let Some(sub) = p.sub.as_ref() { final_sub_clone.as_ref().replace(sub.clone()); } } PathBehavior::Inherit => { if final_behavior.is_delete() { let union: LinkedHashSet = final_add_clone .as_ref() .borrow() .union(p.add.as_ref().unwrap_or(&default)) .filter(|e| !p.sub.as_ref().unwrap_or(&default).contains(*e)) .cloned() .collect(); final_add_clone.as_ref().borrow_mut().extend(union); debug!("inherit final_add: {:?}", final_add_clone.as_ref().borrow()); } else { let union: LinkedHashSet = final_sub_clone .as_ref() .borrow() .union(p.sub.as_ref().unwrap_or(&default)) .filter(|e| !p.add.as_ref().unwrap_or(&default).contains(*e)) .cloned() .collect(); final_sub_clone.as_ref().borrow_mut().extend(union); } } } if !p.default_behavior.is_inherit() { final_behavior = p.default_behavior; } } }); SPathOptions::builder(final_behavior) .add( final_add .clone() .as_ref() .borrow() .iter() .collect::>() .as_slice(), ) .sub( final_sub .clone() .as_ref() .borrow() .iter() .collect::>() .as_slice(), ) .build() } fn get_final_env(&self, cmd_filter: Option) -> SEnvOptions { let mut final_behavior = EnvBehavior::default(); let mut final_set = HashMap::new(); let mut final_keep = LinkedHashSet::new(); let mut final_check = LinkedHashSet::new(); let mut final_delete = LinkedHashSet::new(); let overriden_behavior = cmd_filter.as_ref().and_then(|f| f.env_behavior); self.iter_in_options(|opt| { if let Some(p) = opt.env.borrow().as_ref() { final_behavior = match p.default_behavior { EnvBehavior::Delete | EnvBehavior::Keep => { // policy is to delete, so we add whitelist and remove blacklist final_keep = p .keep .as_ref() .unwrap_or(&LinkedHashSet::new()) .iter() .filter(|e| { //p.set.as_ref().is_some_and(|set| !set.env_matches(e)) || !p.check.env_matches(e) || !p.delete.env_matches(e) }) .cloned() .collect(); final_check = p .check .as_ref() .unwrap_or(&LinkedHashSet::new()) .iter() .filter(|e| { //p.set.as_ref().is_some_and(|set| !set.env_matches(e)) //|| !p.delete.env_matches(e) }) .cloned() .collect(); final_delete = p .delete .as_ref() .unwrap_or(&LinkedHashSet::new()) .iter() .filter(|e| { //p.set.as_ref().is_some_and(|set| !set.env_matches(e)) || !p.check.env_matches(e) }) .cloned() .collect(); if let Some(set) = &p.set { final_set = set.clone(); } debug!("check: {:?}", final_check); p.default_behavior } EnvBehavior::Inherit => { final_keep = final_keep .union(p.keep.as_ref().unwrap_or(&LinkedHashSet::new())) .cloned() .collect(); final_check = final_check .union(p.check.as_ref().unwrap_or(&LinkedHashSet::new())) .cloned() .collect(); final_delete = final_delete .union(p.delete.as_ref().unwrap_or(&LinkedHashSet::new())) .cloned() .collect(); if let Some(set) = &p.set { final_set.extend(set.clone()); } debug!("check: {:?}", final_check); final_behavior } }; } }); SEnvOptions::builder(overriden_behavior.unwrap_or(final_behavior)) .set(final_set) .keep(final_keep) .unwrap() .check(final_check) .unwrap() .delete(final_delete) .unwrap() .build() } fn get_level(&self) -> Level { let (level, _) = self .find_in_options(|opt| Some((opt.level, ()))) .unwrap_or((Level::None, ())); level } pub fn to_opt(&self) -> Rc> { rc_refcell!(Opt::builder(self.get_level()) .path(self.get_final_path()) .env(self.get_final_env(None)) .maybe_root( self.find_in_options(|opt| opt.root.map(|root| (opt.level, root))) .map(|(_, root)| root), ) .maybe_bounding( self.find_in_options(|opt| opt.bounding.map(|bounding| (opt.level, bounding))) .map(|(_, bounding)| bounding), ) .maybe_authentication( self.find_in_options(|opt| { opt.authentication .map(|authentication| (opt.level, authentication)) }) .map(|(_, authentication)| authentication), ) .maybe_timeout( self.find_in_options(|opt| opt.timeout.clone().map(|timeout| (opt.level, timeout))) .map(|(_, timeout)| timeout), ) .build()) } } #[cfg(test)] mod tests { use super::super::options::*; use super::super::structs::*; fn env_key_set_equal(a: I, b: J) -> bool where I: IntoIterator, J: IntoIterator, { let mut a_vec: Vec<_> = a.into_iter().collect(); let mut b_vec: Vec<_> = b.into_iter().collect(); a_vec.sort_by(|a, b| a.value.cmp(&b.value)); b_vec.sort_by(|a, b| a.value.cmp(&b.value)); a_vec == b_vec } fn hashset_vec_equal(a: I, b: J) -> bool where I: IntoIterator, I::Item: Into, J: IntoIterator, J::Item: Into, { let mut a_vec: Vec = a.into_iter().map(Into::into).collect(); let mut b_vec: Vec = b.into_iter().map(Into::into).collect(); a_vec.sort(); b_vec.sort(); a_vec == b_vec } #[test] fn test_find_in_options() { let config = SConfig::builder() .role( SRole::builder("test") .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Inherit) .add(["path2"]) .build(), ) .build() }) .build(), ) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Delete) .add(["path1"]) .build(), ) .build() }) .build(); let options = OptStack::from_role(config.as_ref().borrow().roles[0].clone()); let res: Option<(Level, SPathOptions)> = options.find_in_options(|opt| opt.path.clone().map(|value| (opt.level, value))); assert_eq!( res, Some(( Level::Role, SPathOptions::builder(PathBehavior::Inherit) .add(["path2"]) .build() )) ); } #[test] fn test_env_global_to_task() { let config = SConfig::builder() .role( SRole::builder("test") .task( STask::builder(1) .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Delete) .keep(["env1"]) .unwrap() .build(), ) .build() }) .build(), ) .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Delete) .keep(["env2"]) .unwrap() .build(), ) .build() }) .build(), ) .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Delete) .keep(["env3"]) .unwrap() .build(), ) .build() }) .build(); let binding = OptStack::from_task(config.task("test", 1).unwrap()).to_opt(); let options = binding.as_ref().borrow(); let res = &options.env.as_ref().unwrap().keep; assert!(res .as_ref() .unwrap_or(&LinkedHashSet::new()) .contains(&EnvKey::from("env1"))); } // test to_opt() for OptStack #[test] fn test_to_opt() { let config = SConfig::builder() .role( SRole::builder("test") .task( STask::builder(1) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Inherit) .add(["path3"]) .build(), ) .env( SEnvOptions::builder(EnvBehavior::Inherit) .keep(["env3"]) .unwrap() .build(), ) .root(SPrivileged::User) .bounding(SBounding::Strict) .authentication(SAuthentication::Perform) .timeout( STimeout::builder() .type_field(TimestampType::TTY) .duration(Duration::minutes(3)) .build(), ) .build() }) .build(), ) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Inherit) .add(["path2"]) .build(), ) .env( SEnvOptions::builder(EnvBehavior::Delete) .keep(["env1"]) .unwrap() .build(), ) .root(SPrivileged::Privileged) .bounding(SBounding::Strict) .authentication(SAuthentication::Skip) .timeout( STimeout::builder() .type_field(TimestampType::PPID) .duration(Duration::minutes(2)) .build(), ) .build() }) .build(), ) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Delete) .add(["path1"]) .build(), ) .env( SEnvOptions::builder(EnvBehavior::Delete) .keep(["env2"]) .unwrap() .build(), ) .root(SPrivileged::Privileged) .bounding(SBounding::Ignore) .authentication(SAuthentication::Perform) .timeout( STimeout::builder() .type_field(TimestampType::TTY) .duration(Duration::minutes(1)) .build(), ) .build() }) .build(); let default = LinkedHashSet::new(); let stack = OptStack::from_roles(config.clone()); let opt = stack.to_opt(); let global_options = opt.as_ref().borrow(); assert_eq!( global_options.path.as_ref().unwrap().default_behavior, PathBehavior::Delete ); assert!(hashset_vec_equal( global_options .path .as_ref() .unwrap() .add .as_ref() .unwrap_or(&default) .clone(), vec!["path1"] )); assert_eq!( global_options.env.as_ref().unwrap().default_behavior, EnvBehavior::Delete ); assert!(env_key_set_equal( global_options .env .as_ref() .unwrap() .keep .as_ref() .unwrap_or(&LinkedHashSet::new()) .clone(), vec![EnvKey::from("env2")] )); assert_eq!( global_options .env .as_ref() .unwrap() .keep .as_ref() .unwrap_or(&LinkedHashSet::new()) .iter() .map(|e| e.clone().into()) .collect::>(), vec!["env2".to_string()] ); assert_eq!(global_options.root.unwrap(), SPrivileged::Privileged); assert_eq!(global_options.bounding.unwrap(), SBounding::Ignore); assert_eq!( global_options.authentication.unwrap(), SAuthentication::Perform ); assert_eq!( global_options.timeout.as_ref().unwrap().duration.unwrap(), Duration::minutes(1) ); assert_eq!( global_options.timeout.as_ref().unwrap().type_field.unwrap(), TimestampType::TTY ); let opt = OptStack::from_role(config.clone().role("test").unwrap()).to_opt(); let role_options = opt.as_ref().borrow(); assert_eq!( role_options.path.as_ref().unwrap().default_behavior, PathBehavior::Delete ); assert!(hashset_vec_equal( role_options .path .as_ref() .unwrap() .add .as_ref() .unwrap_or(&default) .clone(), vec!["path1", "path2"] )); assert_eq!( role_options.env.as_ref().unwrap().default_behavior, EnvBehavior::Delete ); assert!(env_key_set_equal( role_options .env .as_ref() .unwrap() .keep .as_ref() .unwrap_or(&LinkedHashSet::new()) .clone(), vec![EnvKey::from("env1")] )); assert_eq!(role_options.root.unwrap(), SPrivileged::Privileged); assert_eq!(role_options.bounding.unwrap(), SBounding::Strict); assert_eq!(role_options.authentication.unwrap(), SAuthentication::Skip); assert_eq!( role_options.timeout.as_ref().unwrap().duration.unwrap(), Duration::minutes(2) ); assert_eq!( role_options.timeout.as_ref().unwrap().type_field.unwrap(), TimestampType::PPID ); let opt = OptStack::from_task(config.task("test", 1).unwrap()).to_opt(); let task_options = opt.as_ref().borrow(); assert_eq!( task_options.path.as_ref().unwrap().default_behavior, PathBehavior::Delete ); assert!(hashset_vec_equal( task_options .path .as_ref() .unwrap() .add .as_ref() .unwrap_or(&default) .clone(), vec!["path1", "path2", "path3"] )); assert_eq!( task_options.env.as_ref().unwrap().default_behavior, EnvBehavior::Delete ); assert!(env_key_set_equal( task_options .env .as_ref() .unwrap() .keep .as_ref() .unwrap_or(&LinkedHashSet::new()) .clone(), vec![EnvKey::from("env1"), EnvKey::from("env3")] )); assert_eq!(task_options.root.unwrap(), SPrivileged::User); assert_eq!(task_options.bounding.unwrap(), SBounding::Strict); assert_eq!( task_options.authentication.unwrap(), SAuthentication::Perform ); assert_eq!( task_options.timeout.as_ref().unwrap().duration.unwrap(), Duration::minutes(3) ); assert_eq!( task_options.timeout.as_ref().unwrap().type_field.unwrap(), TimestampType::TTY ); } #[test] fn is_wildcard_env_key() { assert!(!is_valid_env_name("TEST_.*")); assert!(!is_valid_env_name("123")); assert!(!is_valid_env_name("")); #[cfg(feature = "pcre2")] assert!(is_regex("TEST_.*")); #[cfg(not(feature = "pcre2"))] assert!(!is_regex("TEST_.*")); } #[test] fn test_get_final_env_set_inherit() { let config = SConfig::builder() .role( SRole::builder("test") .task( STask::builder(1) .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Inherit) .set([("env1", "value3")]) .build(), ) .build() }) .build(), ) .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Inherit) .set([("env2", "value2")]) .build(), ) .build() }) .build(), ) .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Delete) .set([("env1", "value1")]) .build(), ) .build() }) .build(); let stack = OptStack::from_task(config.task("test", 1).unwrap()); let opt = stack.to_opt(); let options = opt.as_ref().borrow(); assert_eq!( options .env .as_ref() .unwrap() .set .as_ref() .unwrap_or(&HashMap::new()) .get("env1") .unwrap(), "value3" ); } #[test] fn test_get_final_path_inherit() { let config = SConfig::builder() .role( SRole::builder("test") .task( STask::builder(1) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Inherit) .sub(["/path3"]) .build(), ) .build() }) .build(), ) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Inherit) .sub(["/path2"]) .build(), ) .build() }) .build(), ) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::KeepSafe) .sub(["/path1"]) .build(), ) .build() }) .build(); let stack = OptStack::from_task(config.task("test", 1).unwrap()); let opt = stack.to_opt(); let options = opt.as_ref().borrow(); assert!(options .path .as_ref() .unwrap() .sub .as_ref() .unwrap() .contains("/path1")); assert!(options .path .as_ref() .unwrap() .sub .as_ref() .unwrap() .contains("/path2")); assert!(options .path .as_ref() .unwrap() .sub .as_ref() .unwrap() .contains("/path3")); } #[test] fn test_find_in_options_none() { let config = SConfig::builder() .role( SRole::builder("test") .task(STask::builder(1).build()) .build(), ) .build(); let stack = OptStack::from_task(config.task("test", 1).unwrap()); let res: Option<(Level, SPathOptions)> = stack.find_in_options(|_| None); assert_eq!(res, None); } #[test] fn test_invalid_envkey() { let invalid_env = "3TE(ST_a"; let env_key = EnvKey::new(invalid_env.to_string()); assert!(env_key.is_err()); assert_eq!( env_key.unwrap_err(), format!( "env key {}, must be a valid env, or a valid regex", invalid_env ) ); } } rootasrole-core-3.2.0/src/database/score.rs000064400000000000000000000536101046102023000167660ustar 00000000000000use std::cmp::Ordering; use bon::{builder, Builder}; use strum::EnumIs; use crate::util::{ HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1, HARDENED_ENUM_VALUE_2, HARDENED_ENUM_VALUE_3, HARDENED_ENUM_VALUE_4, }; use super::actor::{DGroupType, DGroups, DUserType, SGroupType, SGroups, SUserType}; #[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, EnumIs, Default)] #[repr(u32)] // Matching user groups for the role pub enum ActorMatchMin { UserMatch = HARDENED_ENUM_VALUE_0, GroupMatch(usize) = HARDENED_ENUM_VALUE_1, #[default] NoMatch = HARDENED_ENUM_VALUE_2, } #[derive(PartialEq, Eq, Clone, Copy, Debug, EnumIs, Default)] #[repr(u32)] pub enum HardenedBool { #[default] False = HARDENED_ENUM_VALUE_0, True = HARDENED_ENUM_VALUE_1, } impl ActorMatchMin { #[inline] pub fn better(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Less } #[inline] pub fn matching(&self) -> bool { *self != ActorMatchMin::NoMatch } } #[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug)] // Matching setuid and setgid for the role pub struct SetuidMin { is_root: bool, } impl From for SetuidMin { fn from(s: SUserType) -> Self { SetuidMin { is_root: user_is_root(&s), } } } impl From<&DUserType<'_>> for SetuidMin { fn from(s: &DUserType) -> Self { SetuidMin { is_root: duser_is_root(s), } } } impl From for SetuidMin { fn from(s: u32) -> Self { SetuidMin { is_root: s == 0 } } } #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub struct SetgidMin { is_root: bool, nb_groups: usize, } impl From for SetgidMin { fn from(s: SGroups) -> Self { SetgidMin { is_root: groups_contains_root(Some(&s)), nb_groups: groups_len(Some(&s)), } } } impl From<&DGroups<'_>> for SetgidMin { fn from(s: &DGroups<'_>) -> Self { SetgidMin { is_root: dgroups_contains_root(Some(s)), nb_groups: dgroups_len(Some(&s)), } } } impl From<&DGroupType<'_>> for SetgidMin { fn from(s: &DGroupType<'_>) -> Self { SetgidMin { is_root: dgroup_is_root(&s), nb_groups: 1, } } } impl From<&Vec> for SetgidMin { fn from(s: &Vec) -> Self { SetgidMin { is_root: s.iter().any(|id| *id == 0), nb_groups: s.len(), } } } impl PartialOrd for SetgidMin { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for SetgidMin { fn cmp(&self, other: &Self) -> Ordering { self.is_root .cmp(&other.is_root) .then_with(|| self.nb_groups.cmp(&other.nb_groups)) } } #[derive(PartialEq, Eq, Clone, Copy, Debug, Default)] pub struct SetUserMin { pub uid: Option, pub gid: Option, } impl PartialOrd for SetUserMin { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for SetUserMin { fn cmp(&self, other: &Self) -> Ordering { self.uid .cmp(&other.uid) .then_with(|| self.gid.cmp(&other.gid)) } } #[derive(PartialEq, Eq, Clone, Copy, Debug, Default, Builder)] #[builder(const)] pub struct CmdMin { #[builder(default = HardenedBool::False, with = || HardenedBool::True, name = "matching")] pub status: HardenedBool, #[builder(default = CmdOrder::empty())] pub order: CmdOrder, } #[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, Default)] pub struct CmdOrder(u32); bitflags::bitflags! { impl CmdOrder: u32 { const WildcardPath = 0b0001; const RegexArgs = 0b0010; const FullRegexArgs = 0b0100; const FullWildcardPath = 0b1000; } } impl CmdMin { pub const MATCH: CmdMin = CmdMin::builder().matching().build(); pub const fn empty() -> Self { CmdMin::builder().build() } pub fn is_empty(&self) -> bool { self.status == HardenedBool::False && self.order.is_empty() } #[inline] pub fn better(&self, other: &Self) -> bool { (self.matching() && !other.matching()) || (self.matching() && self.order.cmp(&other.order) == Ordering::Less) } #[inline] pub fn matching(&self) -> bool { self.status == HardenedBool::True } pub fn set_matching(&mut self) { self.status = HardenedBool::True; } pub fn union_order(&mut self, order: CmdOrder) { self.order |= order; } } #[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, Default)] #[repr(u32)] pub enum CapsMin { #[default] Undefined = HARDENED_ENUM_VALUE_0, NoCaps = HARDENED_ENUM_VALUE_1, CapsNoAdmin(usize) = HARDENED_ENUM_VALUE_2, CapsAdmin(usize) = HARDENED_ENUM_VALUE_3, CapsAll = HARDENED_ENUM_VALUE_4, } #[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Debug, Default)] pub struct SecurityMin(u32); bitflags::bitflags! { impl SecurityMin: u32 { const DisableBounding = 0b000001; const EnableRoot = 0b000010; const KeepEnv = 0b000100; const KeepPath = 0b001000; const KeepUnsafePath = 0b010000; const SkipAuth = 0b100000; } } #[derive(PartialEq, Eq, Clone, Copy, Debug, Default, Builder)] pub struct TaskScore { #[builder(default)] pub cmd_min: CmdMin, #[builder(default)] pub caps_min: CapsMin, #[builder(default)] pub setuser_min: SetUserMin, } #[derive(PartialEq, Eq, Clone, Copy, Debug, Default, Builder)] pub struct Score { pub user_min: ActorMatchMin, pub cmd_min: CmdMin, pub caps_min: CapsMin, pub setuser_min: SetUserMin, pub security_min: SecurityMin, } impl Score { pub fn set_cmd_score(&mut self, cmd_min: CmdMin) { self.cmd_min = cmd_min; } pub fn set_task_score(&mut self, task_score: &TaskScore) { self.cmd_min = task_score.cmd_min; self.caps_min = task_score.caps_min; self.setuser_min = task_score.setuser_min; } pub fn set_role_score(&mut self, role_score: &ActorMatchMin) { self.user_min = *role_score; } pub fn prettyprint(&self) -> String { format!( "{:?}, {:?}, {:?}, {:?}, {:?}", self.user_min, self.cmd_min, self.caps_min, self.setuser_min, self.security_min ) } pub fn user_cmp(&self, other: &Score) -> Ordering { self.user_min.cmp(&other.user_min) } /// Compare the score of tasks results #[inline] pub fn cmd_cmp(&self, other: &Score) -> Ordering { self.cmd_min .order .cmp(&other.cmd_min.order) .then(self.caps_min.cmp(&other.caps_min)) .then(self.setuser_min.cmp(&other.setuser_min)) .then(self.security_min.cmp(&other.security_min)) } #[inline] pub fn user_matching(&self) -> bool { self.user_min != ActorMatchMin::NoMatch } #[inline] pub fn command_matching(&self) -> bool { self.cmd_min.matching() } #[inline] pub fn fully_matching(&self) -> bool { self.user_matching() && self.command_matching() } /// Return true if the score is better than the other #[inline] pub fn better_command(&self, other: &Score) -> bool { (self.command_matching() && !other.command_matching()) || (self.command_matching() && self.cmd_cmp(other) == Ordering::Less) } #[inline] pub fn better_user(&self, other: &Score) -> bool { (self.user_matching() && !other.user_matching()) || (self.user_matching() && self.user_cmp(other) == Ordering::Less) } #[inline] pub fn better_fully(&self, other: &Score) -> bool { (self.fully_matching() && !other.fully_matching()) || (self.fully_matching() && self.cmp(other) == Ordering::Less) } } impl PartialOrd for Score { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Score { fn cmp(&self, other: &Self) -> Ordering { self.cmd_cmp(other).then(self.user_cmp(other)) } fn max(self, other: Self) -> Self { std::cmp::max_by(self, other, Ord::cmp) } fn min(self, other: Self) -> Self { std::cmp::min_by(self, other, Ord::cmp) } fn clamp(self, min: Self, max: Self) -> Self { self.max(min).min(max) } } #[inline] fn group_is_root(actortype: &SGroupType) -> bool { (*actortype).fetch_id().map_or(false, |id| id == 0) } #[inline] fn dgroup_is_root(actortype: &DGroupType<'_>) -> bool { (*actortype).fetch_id().map_or(false, |id| id == 0) } #[inline] fn user_is_root(actortype: &SUserType) -> bool { (*actortype).fetch_id().map_or(false, |id| id == 0) } #[inline] fn duser_is_root(actortype: &DUserType<'_>) -> bool { (*actortype).fetch_id().map_or(false, |id| id == 0) } #[inline] fn groups_contains_root(list: Option<&SGroups>) -> bool { if let Some(list) = list { match list { SGroups::Single(group) => group_is_root(group), SGroups::Multiple(groups) => groups.iter().any(group_is_root), } } else { false } } #[inline] fn dgroups_contains_root(list: Option<&DGroups<'_>>) -> bool { if let Some(list) = list { match list { DGroups::Single(group) => dgroup_is_root(group), DGroups::Multiple(groups) => groups.iter().any(dgroup_is_root), } } else { false } } #[inline] fn groups_len(groups: Option<&SGroups>) -> usize { match groups { Some(groups) => groups.len(), None => 0, } } #[inline] fn dgroups_len(groups: Option<&DGroups<'_>>) -> usize { match groups { Some(groups) => groups.len(), None => 0, } } #[cfg(test)] mod tests { use super::*; use crate::database::actor::{DGroupType, DGroups, DUserType, SGroupType, SGroups, SUserType}; use std::borrow::Cow; #[test] fn test_group_is_root() { let root_group = SGroupType::from(0); let non_root_group = SGroupType::from(1); assert!(group_is_root(&root_group)); assert!(!group_is_root(&non_root_group)); } #[test] fn test_dgroup_is_root() { let root_group = DGroupType::from(0); let non_root_group = DGroupType::from(1); assert!(dgroup_is_root(&root_group)); assert!(!dgroup_is_root(&non_root_group)); } #[test] fn test_user_is_root() { let root_user = SUserType::from(0); let non_root_user = SUserType::from(1); assert!(user_is_root(&root_user)); assert!(!user_is_root(&non_root_user)); } #[test] fn test_duser_is_root() { let root_user = DUserType::from(0); let non_root_user = DUserType::from(1); assert!(duser_is_root(&root_user)); assert!(!duser_is_root(&non_root_user)); } #[test] fn test_groups_contains_root() { let root_group = SGroupType::from(0); let non_root_group = SGroupType::from(1); let single = SGroups::Single(root_group.clone()); let multiple = SGroups::from(vec![non_root_group.clone(), root_group.clone()]); let none = None; assert!(groups_contains_root(Some(&single))); assert!(groups_contains_root(Some(&multiple))); assert!(!groups_contains_root(Some(&SGroups::Single( non_root_group )))); assert!(!groups_contains_root(none)); } #[test] fn test_dgroups_contains_root() { let root_group = DGroupType::from(0); let non_root_group = DGroupType::from(1); let single = DGroups::Single(root_group.clone()); let multiple = DGroups::Multiple(Cow::Owned(vec![non_root_group.clone(), root_group.clone()])); let none = None; assert!(dgroups_contains_root(Some(&single))); assert!(dgroups_contains_root(Some(&multiple))); assert!(!dgroups_contains_root(Some(&DGroups::Single( non_root_group )))); assert!(!dgroups_contains_root(none)); } #[test] fn test_groups_len() { let group1 = SGroupType::from(0); let single = SGroups::Single(group1); let multiple = SGroups::from(vec![SGroupType::from(0), SGroupType::from(1)]); assert_eq!(groups_len(Some(&single)), 1); assert_eq!(groups_len(Some(&multiple)), 2); assert_eq!(groups_len(None), 0); } #[test] fn test_dgroups_len() { let group1 = DGroupType::from(0); let single = DGroups::Single(group1); let multiple = DGroups::Multiple(Cow::Owned(vec![DGroupType::from(0), DGroupType::from(1)])); assert_eq!(dgroups_len(Some(&single)), 1); assert_eq!(dgroups_len(Some(&multiple)), 2); assert_eq!(dgroups_len(None), 0); } #[test] fn test_setgidmin_from_sgroups() { let groups = SGroups::from(vec![SGroupType::from(0), SGroupType::from(1)]); let setgid = SetgidMin::from(groups); assert!(setgid.is_root); assert_eq!(setgid.nb_groups, 2); } #[test] fn test_setgidmin_from_dgroups() { let groups = DGroups::from(vec![DGroupType::from(1), DGroupType::from(2)]); let setgid = SetgidMin::from(&groups); assert!(!setgid.is_root); assert_eq!(setgid.nb_groups, 2); } #[test] fn test_setgidmin_from_vec_u32() { let groups = vec![0, 1, 2]; let setgid = SetgidMin::from(&groups); assert!(setgid.is_root); assert_eq!(setgid.nb_groups, 3); } #[test] fn test_setgidmin_from_dgrouptype() { let group = DGroupType::from(0); let setgid = SetgidMin::from(&group); assert!(setgid.is_root); assert_eq!(setgid.nb_groups, 1); } #[test] fn test_setuidmin_from_susertype() { let user = SUserType::from(0); let setuid = SetuidMin::from(user); assert!(setuid.is_root); } #[test] fn test_setuidmin_from_dusertype() { let user = DUserType::from(1); let setuid = SetuidMin::from(&user); assert!(!setuid.is_root); } #[test] fn test_setuidmin_from_u32() { let setuid = SetuidMin::from(0); assert!(setuid.is_root); let setuid = SetuidMin::from(1); assert!(!setuid.is_root); } #[test] fn test_score_ordering() { let mut score1 = Score::default(); let mut score2 = Score::default(); score1.cmd_min = CmdMin::builder().matching().build(); score2.cmd_min = CmdMin::builder() .matching() .order(CmdOrder::WildcardPath) .build(); assert!(score1 < score2 || score1 == score2 || score1 > score2); } #[test] fn test_score_prettyprint() { let score = Score::default(); let s = score.prettyprint(); assert!(s.contains("NoMatch")); } #[test] fn test_cmdmin_better_and_matching() { let a = CmdMin::builder().matching().build(); let b = CmdMin::builder().build(); assert!(a.matching()); assert!(!b.matching()); assert!(!b.better(&a)); assert!(a.better(&b)); } #[test] fn test_score_better_methods() { let mut score1 = Score::default(); let mut score2 = Score::default(); score1.cmd_min = CmdMin::builder().matching().build(); score2.cmd_min = CmdMin::builder().build(); assert!(score1.better_command(&score2)); assert!(!score2.better_command(&score1)); } #[test] fn test_setuser_min_ordering() { let setuser1 = SetUserMin { uid: Some(SetuidMin::from(0)), gid: Some(SetgidMin::from(&vec![0])), }; let setuser2 = SetUserMin { uid: Some(SetuidMin::from(1)), gid: Some(SetgidMin::from(&vec![1])), }; assert!(setuser1 > setuser2); } #[test] fn test_setgidmin_ordering() { let setgid1 = SetgidMin { is_root: true, nb_groups: 2, }; let setgid2 = SetgidMin { is_root: false, nb_groups: 3, }; assert!(setgid1 > setgid2); assert!(setgid2 < setgid1); assert!(setgid1 != setgid2); let setgid2 = SetgidMin { is_root: true, nb_groups: 3, }; assert!(setgid1 < setgid2); assert!(setgid2 > setgid1); assert!(setgid1 != setgid2); } #[test] fn test_actor_match_min() { let setuser = ActorMatchMin::UserMatch; assert!(setuser.matching()); let setuser_other = ActorMatchMin::NoMatch; assert!(!setuser_other.matching()); assert!(setuser.better(&setuser_other)); } #[test] fn test_security_min() { let security = SecurityMin::empty(); assert!(security.is_empty()); let security_other = SecurityMin::DisableBounding; assert!(!security_other.is_empty()); assert!(security < security_other); assert!(security_other > security); assert!(security_other != security); let security = SecurityMin::EnableRoot; assert!(security > security_other); assert!(security_other < security); assert!(security_other != security); let security_other = SecurityMin::KeepEnv; assert!(security_other > security); assert!(security < security_other); assert!(security_other != security); let security = SecurityMin::KeepPath; assert!(security > security_other); assert!(security_other < security); assert!(security_other != security); let security_other = SecurityMin::KeepUnsafePath; assert!(security_other > security); assert!(security < security_other); assert!(security_other != security); let security = SecurityMin::SkipAuth; assert!(security > security_other); assert!(security_other < security); assert!(security_other != security); let security_other = SecurityMin::empty(); assert!(security > security_other); assert!(security_other < security); } #[test] fn test_set_score() { let mut score = Score::default(); let task_score = TaskScore { cmd_min: CmdMin::builder().matching().build(), caps_min: CapsMin::NoCaps, setuser_min: SetUserMin::default(), }; score.set_task_score(&task_score); assert_eq!(score.cmd_min, CmdMin::builder().matching().build()); assert_eq!(score.caps_min, CapsMin::NoCaps); assert_eq!(score.setuser_min, SetUserMin::default()); let role_score = ActorMatchMin::UserMatch; score.set_role_score(&role_score); assert_eq!(score.user_min, ActorMatchMin::UserMatch); assert_eq!(score.cmd_min, CmdMin::builder().matching().build()); assert_eq!(score.caps_min, CapsMin::NoCaps); assert_eq!(score.setuser_min, SetUserMin::default()); assert_eq!(score.security_min, SecurityMin::empty()); score.set_cmd_score( CmdMin::builder() .matching() .order(CmdOrder::WildcardPath) .build(), ); assert_eq!( score.cmd_min, CmdMin::builder() .matching() .order(CmdOrder::WildcardPath) .build() ); assert_eq!(score.caps_min, CapsMin::NoCaps); assert_eq!(score.setuser_min, SetUserMin::default()); assert_eq!(score.user_min, ActorMatchMin::UserMatch); assert_eq!(score.security_min, SecurityMin::empty()); } #[test] fn test_score_matching() { let mut score = Score::default(); assert!(!score.user_matching()); assert!(!score.command_matching()); assert!(!score.fully_matching()); score.user_min = ActorMatchMin::UserMatch; assert!(score.user_matching()); assert!(!score.command_matching()); assert!(!score.fully_matching()); score.cmd_min = CmdMin::builder().matching().build(); assert!(score.user_matching()); assert!(score.command_matching()); assert!(score.fully_matching()); score.user_min = ActorMatchMin::NoMatch; assert!(!score.user_matching()); assert!(score.command_matching()); assert!(!score.fully_matching()); } #[test] fn test_score_better() { let mut score1 = Score::default(); let mut score2 = Score::default(); score1.cmd_min = CmdMin::builder().matching().build(); score2.cmd_min = CmdMin::builder() .matching() .order(CmdOrder::WildcardPath) .build(); assert!(!score2.better_command(&score1)); assert!(score1.better_command(&score2)); assert!(!score1.better_user(&score2)); assert!(!score2.better_user(&score1)); assert!(!score1.better_fully(&score2)); assert!(!score2.better_fully(&score1)); score1.user_min = ActorMatchMin::UserMatch; score2.user_min = ActorMatchMin::GroupMatch(1); assert!(score1.better_user(&score2)); assert!(!score2.better_user(&score1)); assert!(score1.better_fully(&score2)); assert!(!score2.better_fully(&score1)); } #[test] fn test_score_max_min_clamp() { let mut score1 = Score::default(); let mut score2 = Score::default(); score1.cmd_min = CmdMin::builder().matching().build(); score2.cmd_min = CmdMin::builder() .matching() .order(CmdOrder::WildcardPath) .build(); assert_eq!(score1.max(score2), score2); assert_eq!(score2.max(score1), score2); assert_eq!(score1.min(score2), score1); assert_eq!(score2.min(score1), score1); let mut score3 = Score::default(); score3.cmd_min = CmdMin::builder() .matching() .order(CmdOrder::RegexArgs) .build(); assert_eq!(score1.clamp(score2, score3), score2); assert_eq!(score2.clamp(score1, score3), score2); } } rootasrole-core-3.2.0/src/database/ser.rs000064400000000000000000000471411046102023000164460ustar 00000000000000use serde::{ ser::{SerializeMap, SerializeSeq}, Serialize, }; use super::{is_default, structs::*}; impl Serialize for SConfig { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if serializer.is_human_readable() { let mut map = serializer.serialize_map(None)?; if let Some(options) = &self.options { map.serialize_entry("options", options)?; } if !self.roles.is_empty() { map.serialize_entry("roles", &self.roles)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } else { let mut map = serializer.serialize_map(None)?; if let Some(options) = &self.options { map.serialize_entry("o", options)?; } if !self.roles.is_empty() { map.serialize_entry("r", &self.roles)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } } } impl Serialize for SRole { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if serializer.is_human_readable() { let mut map = serializer.serialize_map(None)?; map.serialize_entry("name", &self.name)?; if let Some(options) = &self.options { map.serialize_entry("options", options)?; } if !self.actors.is_empty() { map.serialize_entry("actors", &self.actors)?; } if !self.tasks.is_empty() { map.serialize_entry("tasks", &self.tasks)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } else { let mut map = serializer.serialize_map(None)?; map.serialize_entry("n", &self.name)?; if let Some(options) = &self.options { map.serialize_entry("o", options)?; } if !self.actors.is_empty() { map.serialize_entry("a", &self.actors)?; } if !self.tasks.is_empty() { map.serialize_entry("t", &self.tasks)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } } } impl Serialize for SetBehavior { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if serializer.is_human_readable() { serializer.serialize_str(&self.to_string()) } else { serializer.serialize_u32(*self as u32) } } } impl Serialize for SSetuidSet { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if serializer.is_human_readable() { let mut map = serializer.serialize_map(None)?; map.serialize_entry("default", &self.default)?; if let Some(fallback) = &self.fallback { map.serialize_entry("fallback", fallback)?; } if !self.add.is_empty() { let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("add", &v)?; } if !self.sub.is_empty() { let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("del", &v)?; } map.end() } else { let mut map = serializer.serialize_map(None)?; map.serialize_entry("d", &(self.default as u32))?; if let Some(fallback) = &self.fallback { map.serialize_entry("f", fallback)?; } if !self.add.is_empty() { let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("a", &v)?; } if !self.sub.is_empty() { let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("s", &v)?; } map.end() } } } impl Serialize for SSetgidSet { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if self.default.is_none() && self.sub.is_empty() && self.add.is_empty() { serializer.serialize_some(&self.fallback) } else if serializer.is_human_readable() { let mut map = serializer.serialize_map(None)?; map.serialize_entry("default", &self.default)?; if !self.fallback.is_empty() { map.serialize_entry("fallback", &self.fallback)?; } if !self.add.is_empty() { let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("add", &v)?; } if !self.sub.is_empty() { let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("del", &v)?; } map.end() } else { let mut map = serializer.serialize_map(None)?; map.serialize_entry("d", &(self.default as u32))?; if !self.fallback.is_empty() { map.serialize_entry("f", &self.fallback)?; } if !self.add.is_empty() { let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("a", &v)?; } if !self.sub.is_empty() { let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("s", &v)?; } map.end() } } } impl Serialize for SCapabilities { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if self.default_behavior.is_none() && self.sub.is_empty() { super::serialize_capset(&self.add, serializer) } else if serializer.is_human_readable() { let mut map = serializer.serialize_map(Some(3))?; if self.default_behavior.is_all() { map.serialize_entry("default", &self.default_behavior)?; } if !self.add.is_empty() { let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("add", &v)?; } if !self.sub.is_empty() { let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("del", &v)?; } map.end() } else { let mut map = serializer.serialize_map(Some(3))?; if self.default_behavior.is_all() { map.serialize_entry("d", &(self.default_behavior as u32))?; } if !self.add.is_empty() { let v: Vec = self.add.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("a", &v)?; } if !self.sub.is_empty() { let v: Vec = self.sub.iter().map(|cap| cap.to_string()).collect(); map.serialize_entry("s", &v)?; } map.end() } } } impl Serialize for SCredentials { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if serializer.is_human_readable() { let mut map = serializer.serialize_map(None)?; if self.setuid.is_some() { map.serialize_entry("setuid", &self.setuid)?; } if self.setgid.is_some() { map.serialize_entry("setgid", &self.setgid)?; } if self.capabilities.is_some() { map.serialize_entry("capabilities", &self.capabilities)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } else { let mut map = serializer.serialize_map(None)?; if self.setuid.is_some() { map.serialize_entry("u", &self.setuid)?; } if self.setgid.is_some() { map.serialize_entry("g", &self.setgid)?; } if self.capabilities.is_some() { map.serialize_entry("c", &self.capabilities)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } } } impl Serialize for STask { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if serializer.is_human_readable() { let mut map = serializer.serialize_map(None)?; map.serialize_entry("name", &self.name)?; if let Some(options) = &self.options { map.serialize_entry("options", options)?; } if let Some(purpose) = &self.purpose { map.serialize_entry("purpose", purpose)?; } if !is_default(&self.cred) { map.serialize_entry("cred", &self.cred)?; } if !cmds_is_default(&self.commands) { map.serialize_entry("commands", &self.commands)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } else { let mut map = serializer.serialize_map(None)?; map.serialize_entry("n", &self.name)?; if let Some(options) = &self.options { map.serialize_entry("o", options)?; } if let Some(purpose) = &self.purpose { map.serialize_entry("p", purpose)?; } if !is_default(&self.cred) { map.serialize_entry("i", &self.cred)?; } if !cmds_is_default(&self.commands) { map.serialize_entry("c", &self.commands)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } } } impl Serialize for SCommands { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { if self.sub.is_empty() && self._extra_fields.is_empty() { if self.add.is_empty() { return serializer.serialize_str( if self .default_behavior .as_ref() .is_some_and(|b| *b == SetBehavior::All) { "all" } else { "none" }, ); } else if !self.add.is_empty() && self .default_behavior .as_ref() .is_none_or(|b| *b == SetBehavior::None) { let mut seq = serializer.serialize_seq(Some(self.add.len()))?; for cmd in &self.add { seq.serialize_element(cmd)?; } return seq.end(); } } if serializer.is_human_readable() { let mut map = serializer.serialize_map(Some(3))?; if self.default_behavior.is_none() { map.serialize_entry("default", &self.default_behavior)?; } if !self.add.is_empty() { map.serialize_entry("add", &self.add)?; } if !self.sub.is_empty() { map.serialize_entry("del", &self.sub)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } else { let mut map = serializer.serialize_map(Some(3))?; if let Some(behavior) = &self.default_behavior { map.serialize_entry("d", &(*behavior as u32))?; } if !self.add.is_empty() { map.serialize_entry("a", &self.add)?; } if !self.sub.is_empty() { map.serialize_entry("s", &self.sub)?; } for (key, value) in &self._extra_fields { map.serialize_entry(key, value)?; } map.end() } } } #[cfg(test)] mod tests { use capctl::Cap; use serde_json::{json, to_value}; use crate::database::actor::SActor; use super::*; #[test] fn test_sconfig_human_readable() { let config = SConfig { options: Some(Default::default()), roles: vec![], _extra_fields: Default::default(), }; let value = to_value(&config).unwrap(); assert!(value.get("options").is_some()); } #[test] fn test_srole_binary() { let role = SRole::builder("admin") .actor(SActor::user(0).build()) .task(STask::builder("test").build()) .options(|o| { o.bounding(crate::database::options::SBounding::Ignore) .build() }) .build(); //cbor4ii encode let bin: Vec = Vec::new(); let mut writer = cbor4ii::core::utils::BufWriter::new(bin); let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); role.serialize(&mut serializer).unwrap(); assert!(!writer.buffer().is_empty()); assert!(!writer .buffer() .windows("tasks".len()) .any(|window| window == "tasks".as_bytes())); assert!(!writer .buffer() .windows("name".len()) .any(|window| window == "name".as_bytes())); assert!(!writer .buffer() .windows("options".len()) .any(|window| window == "options".as_bytes())); assert!(!writer .buffer() .windows("actors".len()) .any(|window| window == "actors".as_bytes())); } #[test] fn test_setbehavior_serialize() { let b = SetBehavior::All; let value = to_value(b).unwrap(); assert_eq!(value, json!("all")); let b = SetBehavior::None; let bin: Vec = Vec::new(); let mut writer = cbor4ii::core::utils::BufWriter::new(bin); let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); b.serialize(&mut serializer).unwrap(); assert!(!writer.buffer().is_empty()); // split HARDENED_ENUM_VALUE_0 to an array of bytes // cbor4ii add 0x1A prefix to the value let splitted = [0x1A, 0x05, 0x2A, 0x29, 0x25]; println!("splitted: {:?}", splitted); println!("buffer: {:?}", writer.buffer()); assert!(writer.buffer() == splitted); // test serialization of SetBehavior::All let b = SetBehavior::All; let bin: Vec = Vec::new(); let mut writer = cbor4ii::core::utils::BufWriter::new(bin); let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); b.serialize(&mut serializer).unwrap(); assert!(!writer.buffer().is_empty()); // split HARDENED_ENUM_VALUE_0 to an array of bytes // cbor4ii add 0x1A prefix to the value let splitted = [0x1A, 0x0A, 0xD5, 0xD6, 0xDA]; println!("splitted: {:?}", splitted); println!("buffer: {:?}", writer.buffer()); assert!(writer.buffer() == splitted); } #[test] fn test_ssetuidset_human_readable() { let set = SSetuidSet::builder() .default(SetBehavior::None) .fallback(1) .add(vec![1.into(), 3.into()]) .sub(vec![4.into(), 5.into()]) .build(); let value = to_value(&set).unwrap(); assert!(value.get("add").is_some()); } #[test] fn test_ssetgidset_seq() { let set = SSetgidSet::builder(SetBehavior::None, vec![0, 1]).build(); let value = to_value(&set).unwrap(); assert!(value.is_array()); assert_eq!(value.as_array().unwrap().len(), 2); assert_eq!(value.as_array().unwrap()[0], json!(0)); assert_eq!(value.as_array().unwrap()[1], json!(1)); } #[test] fn test_scapabilities_minimal() { let caps = SCapabilities::builder(SetBehavior::None) .add_cap(Cap::SYS_ADMIN) .build(); let value = to_value(&caps).unwrap(); assert!(value.is_array()); } #[test] fn test_scredentials_human_readable() { let creds = SCredentials::builder() .setuid(1) .setgid(2) .capabilities( SCapabilities::builder(SetBehavior::None) .add_cap(Cap::SYS_ADMIN) .build(), ) .build(); let value = to_value(&creds).unwrap(); assert!(value.get("setuid").is_some()); assert!(value.get("setgid").is_some()); assert!(value.get("capabilities").is_some()); } #[test] fn test_stask_binary() { let task = STask::builder("test") .options(|o| { o.bounding(crate::database::options::SBounding::Ignore) .build() }) .cred(SCredentials::builder().setuid(1).setgid(2).build()) .commands( SCommands::builder(SetBehavior::All) .add(vec!["ls".into()]) .build(), ) .build(); let bin: Vec = Vec::new(); let mut writer = cbor4ii::core::utils::BufWriter::new(bin); let mut serializer = cbor4ii::serde::Serializer::new(&mut writer); task.serialize(&mut serializer).unwrap(); assert!(!writer.buffer().is_empty()); assert!(!writer .buffer() .windows("name".len()) .any(|window| window == "name".as_bytes())); assert!(!writer .buffer() .windows("options".len()) .any(|window| window == "options".as_bytes())); assert!(!writer .buffer() .windows("cred".len()) .any(|window| window == "cred".as_bytes())); assert!(!writer .buffer() .windows("commands".len()) .any(|window| window == "commands".as_bytes())); assert!(writer .buffer() .windows("test".len()) .any(|window| window == "test".as_bytes())); } #[test] fn test_scommands_all_none() { let cmds = SCommands { default_behavior: Some(SetBehavior::All), add: vec![], sub: vec![], _extra_fields: Default::default(), }; let value = to_value(&cmds).unwrap(); assert!(value.is_string()); assert_eq!(value, json!("all")); let cmds = SCommands { default_behavior: Some(SetBehavior::None), add: vec![], sub: vec![], _extra_fields: Default::default(), }; let value = to_value(&cmds).unwrap(); assert!(value.is_string()); assert_eq!(value, json!("none")); } #[test] fn test_scommands_seq() { let cmds = SCommands::builder(SetBehavior::None) .add(vec!["ls".into()]) .build(); let value = to_value(&cmds).unwrap(); assert!(value.is_array()); } } rootasrole-core-3.2.0/src/database/structs.rs000064400000000000000000001107471046102023000173670ustar 00000000000000use bon::{bon, builder, Builder}; use capctl::{Cap, CapSet}; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; use strum::{Display, EnumIs, EnumString, FromRepr}; use std::{ cell::RefCell, error::Error, fmt, ops::{Index, Not}, rc::{Rc, Weak}, }; use crate::{ rc_refcell, util::{HARDENED_ENUM_VALUE_0, HARDENED_ENUM_VALUE_1}, }; use super::{ actor::{SActor, SGroupType, SGroups, SUserType}, options::{Level, Opt, OptBuilder}, }; #[derive(Deserialize, PartialEq, Eq, Debug)] pub struct SConfig { #[serde(default, deserialize_with = "sconfig_opt", alias = "o")] pub options: Option>>, #[serde(default, alias = "r")] pub roles: Vec>>, #[serde(default, flatten)] pub _extra_fields: Map, } fn sconfig_opt<'de, D>(deserializer: D) -> Result>>, D::Error> where D: Deserializer<'de>, { let opt: Option>> = Option::deserialize(deserializer)?; if let Some(opt) = opt { opt.as_ref().borrow_mut().level = Level::Global; Ok(Some(opt)) } else { Ok(None) } } #[derive(Deserialize, Debug, Derivative)] #[serde(rename_all = "kebab-case")] #[derivative(PartialEq, Eq)] pub struct SRole { #[serde(default, skip_serializing_if = "String::is_empty")] pub name: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub actors: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tasks: Vec>>, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "srole_opt" )] pub options: Option>>, #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] pub _extra_fields: Map, #[serde(skip)] #[derivative(PartialEq = "ignore")] pub _config: Option>>, } fn srole_opt<'de, D>(deserializer: D) -> Result>>, D::Error> where D: Deserializer<'de>, { let opt: Option>> = Option::deserialize(deserializer)?; if let Some(opt) = opt { opt.as_ref().borrow_mut().level = Level::Role; Ok(Some(opt)) } else { Ok(None) } } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] #[serde(untagged)] pub enum IdTask { Name(String), Number(usize), } impl std::fmt::Display for IdTask { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { IdTask::Name(name) => write!(f, "{}", name), IdTask::Number(id) => write!(f, "{}", id), } } } pub(super) fn cmds_is_default(cmds: &SCommands) -> bool { cmds.default_behavior .as_ref() .is_none_or(|b| *b == Default::default()) && cmds.add.is_empty() && cmds.sub.is_empty() && cmds._extra_fields.is_empty() } #[derive(Deserialize, Debug, Derivative)] #[derivative(PartialEq, Eq)] pub struct STask { #[serde(alias = "n", default, skip_serializing_if = "IdTask::is_number")] pub name: IdTask, #[serde(alias = "p", skip_serializing_if = "Option::is_none")] pub purpose: Option, #[serde(alias = "i", default, skip_serializing_if = "is_default")] pub cred: SCredentials, #[serde(alias = "c", default, skip_serializing_if = "cmds_is_default")] pub commands: SCommands, #[serde( alias = "o", default, skip_serializing_if = "Option::is_none", deserialize_with = "stask_opt" )] pub options: Option>>, #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] pub _extra_fields: Map, #[serde(skip)] #[derivative(PartialEq = "ignore")] pub _role: Option>>, } fn stask_opt<'de, D>(deserializer: D) -> Result>>, D::Error> where D: Deserializer<'de>, { let opt: Option>> = Option::deserialize(deserializer)?; if let Some(opt) = opt { opt.as_ref().borrow_mut().level = Level::Task; Ok(Some(opt)) } else { Ok(None) } } #[derive(Deserialize, Debug, Builder, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct SCredentials { #[serde(alias = "u", skip_serializing_if = "Option::is_none")] #[builder(into)] pub setuid: Option, #[serde(alias = "g", skip_serializing_if = "Option::is_none")] #[builder(into)] pub setgid: Option, #[serde(default, alias = "c", skip_serializing_if = "Option::is_none")] pub capabilities: Option, #[serde(default, flatten, skip_serializing_if = "Map::is_empty")] #[builder(default)] pub _extra_fields: Map, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(untagged)] pub enum SUserEither { MandatoryUser(SUserType), UserSelector(SSetuidSet), } impl From for SUserEither { fn from(actor: SUserType) -> Self { SUserEither::MandatoryUser(actor) } } impl From for SUserEither { fn from(set: SSetuidSet) -> Self { SUserEither::UserSelector(set) } } impl From<&str> for SUserEither { fn from(name: &str) -> Self { SUserEither::MandatoryUser(name.into()) } } impl From for SUserEither { fn from(id: u32) -> Self { SUserEither::MandatoryUser(id.into()) } } #[derive(Deserialize, Debug, Clone, Builder, PartialEq, Eq)] pub struct SSetuidSet { #[serde( alias = "d", rename = "default", default, skip_serializing_if = "is_default" )] #[builder(default)] pub default: SetBehavior, #[builder(into)] #[serde(alias = "f", skip_serializing_if = "Option::is_none")] pub fallback: Option, #[serde(default, alias = "a", skip_serializing_if = "Vec::is_empty")] #[builder(default, with = FromIterator::from_iter)] pub add: Vec, #[serde( default, alias = "del", alias = "s", skip_serializing_if = "Vec::is_empty" )] #[builder(default, with = FromIterator::from_iter)] pub sub: Vec, } #[derive(PartialEq, Eq, Display, Debug, EnumIs, Clone, Copy, FromRepr, EnumString)] #[strum(serialize_all = "lowercase")] #[derive(Default)] #[repr(u32)] pub enum SetBehavior { #[default] None = HARDENED_ENUM_VALUE_0, All = HARDENED_ENUM_VALUE_1, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(untagged)] pub enum SGroupsEither { MandatoryGroup(SGroupType), MandatoryGroups(SGroups), GroupSelector(SSetgidSet), } impl From for SGroupsEither { fn from(group: SGroups) -> Self { SGroupsEither::MandatoryGroups(group) } } impl From for SGroupsEither { fn from(set: SSetgidSet) -> Self { SGroupsEither::GroupSelector(set) } } impl From<&str> for SGroupsEither { fn from(name: &str) -> Self { SGroupsEither::MandatoryGroup(name.into()) } } impl From for SGroupsEither { fn from(id: u32) -> Self { SGroupsEither::MandatoryGroup(id.into()) } } #[derive(Deserialize, Debug, Clone, Builder, PartialEq, Eq)] pub struct SSetgidSet { #[serde( rename = "default", alias = "d", default, skip_serializing_if = "is_default" )] #[builder(start_fn)] pub default: SetBehavior, #[serde(alias = "f")] #[builder(start_fn, into)] pub fallback: SGroups, #[serde(default, alias = "a", skip_serializing_if = "Vec::is_empty")] #[builder(default, with = FromIterator::from_iter)] pub add: Vec, #[serde(default, alias = "s", skip_serializing_if = "Vec::is_empty")] #[builder(default, with = FromIterator::from_iter)] pub sub: Vec, } #[derive(PartialEq, Eq, Debug, Builder)] pub struct SCapabilities { #[builder(start_fn)] pub default_behavior: SetBehavior, #[builder(field)] pub add: CapSet, #[builder(field)] pub sub: CapSet, } impl SCapabilitiesBuilder { pub fn add_cap(mut self, cap: Cap) -> Self { self.add.add(cap); self } pub fn add_all(mut self, set: CapSet) -> Self { self.add = set; self } pub fn sub_cap(mut self, cap: Cap) -> Self { self.sub.add(cap); self } pub fn sub_all(mut self, set: CapSet) -> Self { self.sub = set; self } } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, EnumIs, Clone)] #[serde(untagged)] pub enum SCommand { Simple(String), Complex(Value), } #[derive(PartialEq, Eq, Debug)] pub struct SCommands { pub default_behavior: Option, pub add: Vec, pub sub: Vec, pub _extra_fields: Map, } // ------------------------ // Default implementations // ------------------------ impl Default for SConfig { fn default() -> Self { SConfig { options: Some(Rc::new(RefCell::new(Opt::default()))), roles: Vec::new(), _extra_fields: Map::default(), } } } impl Default for SRole { fn default() -> Self { SRole { name: "".to_string(), actors: Vec::new(), tasks: Vec::new(), options: None, _extra_fields: Map::default(), _config: None, } } } impl Default for STask { fn default() -> Self { STask { name: IdTask::Number(0), purpose: None, cred: SCredentials::default(), commands: SCommands::default(), options: None, _extra_fields: Map::default(), _role: None, } } } impl Default for SCredentials { fn default() -> Self { SCredentials { setuid: None, setgid: None, capabilities: Some(SCapabilities::default()), _extra_fields: Map::default(), } } } impl Default for SCommands { fn default() -> Self { SCommands { default_behavior: Some(SetBehavior::default()), add: Vec::new(), sub: Vec::new(), _extra_fields: Map::default(), } } } impl Default for SCapabilities { fn default() -> Self { SCapabilities { default_behavior: SetBehavior::default(), add: CapSet::empty(), sub: CapSet::empty(), } } } impl Default for SSetuidSet { fn default() -> Self { SSetuidSet::builder().build() } } impl Default for IdTask { fn default() -> Self { IdTask::Number(0) } } // ------------------------ // From implementations // ------------------------ impl From for IdTask { fn from(id: usize) -> Self { IdTask::Number(id) } } impl From for IdTask { fn from(name: String) -> Self { IdTask::Name(name) } } impl From<&str> for IdTask { fn from(name: &str) -> Self { IdTask::Name(name.to_string()) } } impl From<&str> for SCommand { fn from(name: &str) -> Self { SCommand::Simple(name.to_string()) } } impl From for SCapabilities { fn from(capset: CapSet) -> Self { SCapabilities { add: capset, ..Default::default() } } } // ------------------------ // Deserialize // ------------------------ // This try to deserialize a number as an ID and a string as a name // ======================== // Implementations for Struct navigation // ======================== #[bon] impl SConfig { #[builder] pub fn new( #[builder(field)] roles: Vec>>, #[builder(with = |f : fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Global))))] options: Option>>, _extra_fields: Option>, ) -> Rc> { let c = Rc::new(RefCell::new(SConfig { roles: roles.clone(), options: options.clone(), _extra_fields: _extra_fields.unwrap_or_default().clone(), })); for role in &roles { role.borrow_mut()._config = Some(Rc::downgrade(&c)); } c } } pub trait RoleGetter { fn role(&self, name: &str) -> Option>>; fn task>( &self, role: &str, name: T, ) -> Result>, Box>; } pub trait TaskGetter { fn task(&self, name: &IdTask) -> Option>>; } impl RoleGetter for Rc> { fn role(&self, name: &str) -> Option>> { self.as_ref() .borrow() .roles .iter() .find(|role| role.borrow().name == name) .cloned() } fn task>( &self, role: &str, name: T, ) -> Result>, Box> { let name = name.into(); self.role(role) .and_then(|role| role.as_ref().borrow().task(&name).cloned()) .ok_or_else(|| format!("Task {} not found in role {}", name, role).into()) } } impl TaskGetter for Rc> { fn task(&self, name: &IdTask) -> Option>> { self.as_ref() .borrow() .tasks .iter() .find(|task| task.borrow().name == *name) .cloned() } } impl SConfigBuilder { pub fn role(mut self, role: Rc>) -> Self { self.roles.push(role); self } pub fn roles(mut self, roles: impl IntoIterator>>) -> Self { self.roles.extend(roles); self } } impl SRoleBuilder { pub fn task(mut self, task: Rc>) -> Self { self.tasks.push(task); self } pub fn actor(mut self, actor: SActor) -> Self { self.actors.push(actor); self } pub fn actors(mut self, actors: impl IntoIterator) -> Self { self.actors.extend(actors); self } pub fn tasks(mut self, tasks: impl IntoIterator>>) -> Self { self.tasks.extend(tasks); self } } #[bon] impl SRole { #[builder] pub fn new( #[builder(start_fn, into)] name: String, #[builder(field)] tasks: Vec>>, #[builder(field)] actors: Vec, #[builder(with = |f : fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Role))))] options: Option>>, #[builder(default)] _extra_fields: Map, ) -> Rc> { let s = Rc::new(RefCell::new(SRole { name, actors, tasks, options, _extra_fields, _config: None, })); for task in s.as_ref().borrow_mut().tasks.iter() { task.borrow_mut()._role = Some(Rc::downgrade(&s)); } s } pub fn config(&self) -> Option>> { self._config.as_ref()?.upgrade() } pub fn task(&self, name: &IdTask) -> Option<&Rc>> { self.tasks .iter() .find(|task| task.as_ref().borrow().name == *name) } } #[bon] impl STask { #[builder] pub fn new( #[builder(start_fn, into)] name: IdTask, purpose: Option, #[builder(default)] cred: SCredentials, #[builder(default)] commands: SCommands, #[builder(with = |f : fn(OptBuilder) -> Opt | rc_refcell!(f(Opt::builder(Level::Task))))] options: Option>>, #[builder(default)] _extra_fields: Map, _role: Option>>, ) -> Rc> { Rc::new(RefCell::new(STask { name, purpose, cred, commands, options, _extra_fields, _role, })) } pub fn role(&self) -> Option>> { self._role.as_ref()?.upgrade() } } impl Index for SConfig { type Output = Rc>; fn index(&self, index: usize) -> &Self::Output { &self.roles[index] } } impl Index for SRole { type Output = Rc>; fn index(&self, index: usize) -> &Self::Output { &self.tasks[index] } } #[bon] impl SCommands { #[builder] pub fn new( #[builder(start_fn)] default_behavior: SetBehavior, #[builder(default, with = FromIterator::from_iter)] add: Vec, #[builder(default, with = FromIterator::from_iter)] sub: Vec, #[builder(default, with = <_>::from_iter)] _extra_fields: Map, ) -> Self { SCommands { default_behavior: Some(default_behavior), add, sub, _extra_fields, } } } impl SCapabilities { pub fn to_capset(&self) -> CapSet { let mut capset = match self.default_behavior { SetBehavior::All => capctl::bounding::probe() & CapSet::not(CapSet::empty()), SetBehavior::None => CapSet::empty(), }; capset = capset.union(self.add); capset.drop_all(self.sub); capset } } /* Confusing impl PartialEq for SUserChooser { fn eq(&self, other: &str) -> bool { match self { SUserChooser::Actor(actor) => actor == &SUserType::from(other), SUserChooser::ChooserStruct(chooser) => chooser.fallback.as_ref().is_some_and(|f| *f == *other), } } }*/ #[cfg(test)] mod tests { use capctl::Cap; use chrono::Duration; use linked_hash_set::LinkedHashSet; use crate::{ as_borrow, database::{ actor::SGroupType, options::{ EnvBehavior, PathBehavior, SAuthentication, SBounding, SEnvOptions, SPathOptions, SPrivileged, STimeout, TimestampType, }, }, }; use super::*; #[test] fn test_deserialize() { let config = r#" { "options": { "path": { "default": "delete", "add": ["path_add"], "sub": ["path_sub"] }, "env": { "default": "delete", "override_behavior": true, "keep": ["keep_env"], "check": ["check_env"] }, "root": "privileged", "bounding": "ignore", "authentication": "skip", "timeout": { "type": "ppid", "duration": "00:05:00" } }, "roles": [ { "name": "role1", "actors": [ { "type": "user", "name": "user1" }, { "type":"group", "groups": ["group1","1000"] } ], "tasks": [ { "name": "task1", "purpose": "purpose1", "cred": { "setuid": { "fallback": "user1", "default": "all", "add": ["user2"], "sub": ["user3"] }, "setgid": "setgid1", "capabilities": { "default": "all", "add": ["cap_net_bind_service"], "sub": ["cap_sys_admin"] } }, "commands": { "default": "all", "add": ["cmd1"], "sub": ["cmd2"] } } ] } ] } "#; let config: SConfig = serde_json::from_str(config).unwrap(); let options = config.options.as_ref().unwrap().as_ref().borrow(); let path = options.path.as_ref().unwrap(); assert_eq!(path.default_behavior, PathBehavior::Delete); let default = LinkedHashSet::new(); assert!(path .add .as_ref() .unwrap_or(&default) .front() .is_some_and(|s| s == "path_add")); let env = options.env.as_ref().unwrap(); assert_eq!(env.default_behavior, EnvBehavior::Delete); assert!(env.override_behavior.is_some_and(|b| b)); assert!(env .keep .as_ref() .unwrap_or(&LinkedHashSet::new()) .front() .is_some_and(|s| s == "keep_env")); assert!(env .check .as_ref() .unwrap_or(&LinkedHashSet::new()) .front() .is_some_and(|s| s == "check_env")); assert!(options.root.as_ref().unwrap().is_privileged()); assert!(options.bounding.as_ref().unwrap().is_ignore()); assert_eq!(options.authentication, Some(SAuthentication::Skip)); let timeout = options.timeout.as_ref().unwrap(); assert_eq!(timeout.type_field, Some(TimestampType::PPID)); assert_eq!(timeout.duration, Some(Duration::minutes(5))); assert_eq!(config.roles[0].as_ref().borrow().name, "role1"); let actor0 = &config.roles[0].as_ref().borrow().actors[0]; assert_eq!( actor0, &SActor::User { id: Some("user1".into()), _extra_fields: Map::default() } ); let actor1 = &config.roles[0].as_ref().borrow().actors[1]; match actor1 { SActor::Group { groups, .. } => match groups.as_ref().unwrap() { SGroups::Multiple(groups) => { assert_eq!(&groups[0], "group1"); assert_eq!(groups[1], 1000); } _ => panic!("unexpected actor group type"), }, _ => panic!("unexpected actor {:?}", actor1), } let role = config.roles[0].as_ref().borrow(); assert_eq!(as_borrow!(role[0]).purpose.as_ref().unwrap(), "purpose1"); let cred = &as_borrow!(&role[0]).cred; let setuidstruct = SSetuidSet::builder() .fallback("user1") .default(SetBehavior::All) .add(["user2".into()]) .sub(["user3".into()]) .build(); assert!( matches!(cred.setuid.as_ref().unwrap(), SUserEither::UserSelector(set) if set == &setuidstruct) ); assert_eq!( *cred.setgid.as_ref().unwrap(), SGroupsEither::MandatoryGroup(SGroupType::from("setgid1")) ); let capabilities = cred.capabilities.as_ref().unwrap(); assert_eq!(capabilities.default_behavior, SetBehavior::All); assert!(capabilities.add.has(Cap::NET_BIND_SERVICE)); assert!(capabilities.sub.has(Cap::SYS_ADMIN)); let commands = &as_borrow!(&role[0]).commands; assert_eq!( *commands.default_behavior.as_ref().unwrap(), SetBehavior::All ); assert_eq!(commands.add[0], SCommand::Simple("cmd1".into())); assert_eq!(commands.sub[0], SCommand::Simple("cmd2".into())); } #[test] fn test_unknown_fields() { let config = r#" { "options": { "path": { "default": "delete", "add": ["path_add"], "sub": ["path_sub"], "unknown": "unknown" }, "env": { "default": "delete", "keep": ["keep_env"], "check": ["check_env"], "unknown": "unknown" }, "allow-root": false, "allow-bounding": false, "timeout": { "type": "ppid", "duration": "00:05:00", "unknown": "unknown" }, "unknown": "unknown" }, "roles": [ { "name": "role1", "actors": [ { "type": "user", "name": "user1", "unknown": "unknown" }, { "type":"bla", "unknown": "unknown" } ], "tasks": [ { "name": "task1", "purpose": "purpose1", "cred": { "setuid": "setuid1", "setgid": "setgid1", "capabilities": { "default": "all", "add": ["cap_dac_override"], "sub": ["cap_dac_override"] }, "unknown": "unknown" }, "commands": { "default": "all", "add": ["cmd1"], "sub": ["cmd2"], "unknown": "unknown" }, "unknown": "unknown" } ], "unknown": "unknown" } ], "unknown": "unknown" } "#; let config: SConfig = serde_json::from_str(config).unwrap(); assert_eq!(config._extra_fields.get("unknown").unwrap(), "unknown"); let binding = config.options.unwrap(); let options = binding.as_ref().borrow(); let env = &options.env.as_ref().unwrap(); assert_eq!(env._extra_fields.get("unknown").unwrap(), "unknown"); assert_eq!(options._extra_fields.get("unknown").unwrap(), "unknown"); let timeout = options.timeout.as_ref().unwrap(); assert_eq!(timeout._extra_fields.get("unknown").unwrap(), "unknown"); assert_eq!(config._extra_fields.get("unknown").unwrap(), "unknown"); let actor0 = &as_borrow!(config.roles[0]).actors[0]; match actor0 { SActor::User { id, _extra_fields } => { assert_eq!(id.as_ref().unwrap(), "user1"); assert_eq!(_extra_fields.get("unknown").unwrap(), "unknown"); } _ => panic!("unexpected actor type"), } let actor1 = &as_borrow!(config.roles[0]).actors[1]; match actor1 { SActor::Unknown(unknown) => { let obj = unknown.as_object().unwrap(); assert_eq!(obj.get("type").unwrap().as_str().unwrap(), "bla"); assert_eq!(obj.get("unknown").unwrap().as_str().unwrap(), "unknown"); } _ => panic!("unexpected actor type"), } assert_eq!( config.roles[0].as_ref().borrow()[0] .as_ref() .borrow() ._extra_fields .get("unknown") .as_ref() .unwrap() .as_str() .unwrap(), "unknown" ); let role = config.roles[0].as_ref().borrow(); let cred = &role[0].as_ref().borrow().cred; assert_eq!(cred._extra_fields.get("unknown").unwrap(), "unknown"); let commands = &as_borrow!(role[0]).commands; assert_eq!(commands._extra_fields.get("unknown").unwrap(), "unknown"); } #[test] fn test_deserialize_alias() { let config = r#" { "options": { "path": { "default": "delete", "add": ["path_add"], "del": ["path_sub"] }, "env": { "default": "delete", "keep": ["keep_env"], "check": ["check_env"] }, "root": "privileged", "bounding": "ignore", "authentication": "skip", "timeout": { "type": "ppid", "duration": "00:05:00" } }, "roles": [ { "name": "role1", "actors": [ { "type": "user", "name": "user1" }, { "type":"group", "groups": ["group1","1000"] } ], "tasks": [ { "name": "task1", "purpose": "purpose1", "cred": { "setuid": "setuid1", "setgid": "setgid1", "capabilities": ["cap_net_bind_service"] }, "commands": { "default": "all", "add": ["cmd1"], "del": ["cmd2"] } } ] } ] } "#; let config: SConfig = serde_json::from_str(config).unwrap(); let options = config.options.as_ref().unwrap().as_ref().borrow(); let path = options.path.as_ref().unwrap(); assert_eq!(path.default_behavior, PathBehavior::Delete); let default = LinkedHashSet::new(); assert!(path .add .as_ref() .unwrap_or(&default) .front() .is_some_and(|s| s == "path_add")); let env = options.env.as_ref().unwrap(); assert_eq!(env.default_behavior, EnvBehavior::Delete); assert!(env .keep .as_ref() .unwrap() .front() .is_some_and(|s| s == "keep_env")); assert!(env .check .as_ref() .unwrap() .front() .is_some_and(|s| s == "check_env")); assert!(options.root.as_ref().unwrap().is_privileged()); assert!(options.bounding.as_ref().unwrap().is_ignore()); assert_eq!(options.authentication, Some(SAuthentication::Skip)); let timeout = options.timeout.as_ref().unwrap(); assert_eq!(timeout.type_field, Some(TimestampType::PPID)); assert_eq!(timeout.duration, Some(Duration::minutes(5))); assert_eq!(config.roles[0].as_ref().borrow().name, "role1"); let actor0 = &config.roles[0].as_ref().borrow().actors[0]; match actor0 { SActor::User { id, .. } => { assert_eq!(id.as_ref().unwrap(), "user1"); } _ => panic!("unexpected actor type"), } let actor1 = &config.roles[0].as_ref().borrow().actors[1]; match actor1 { SActor::Group { groups, .. } => match groups.as_ref().unwrap() { SGroups::Multiple(groups) => { assert_eq!(groups[0], SGroupType::from("group1")); assert_eq!(groups[1], SGroupType::from(1000)); } _ => panic!("unexpected actor group type"), }, _ => panic!("unexpected actor {:?}", actor1), } let role = config.roles[0].as_ref().borrow(); assert_eq!(as_borrow!(role[0]).purpose.as_ref().unwrap(), "purpose1"); let cred = &as_borrow!(&role[0]).cred; assert_eq!( cred.setuid.as_ref().unwrap(), &SUserEither::from(SUserType::from("setuid1")) ); assert_eq!( *cred.setgid.as_ref().unwrap(), SGroupsEither::MandatoryGroup(SGroupType::from("setgid1")) ); let capabilities = cred.capabilities.as_ref().unwrap(); assert_eq!(capabilities.default_behavior, SetBehavior::None); assert!(capabilities.add.has(Cap::NET_BIND_SERVICE)); assert!(capabilities.sub.is_empty()); let commands = &as_borrow!(&role[0]).commands; assert_eq!( *commands.default_behavior.as_ref().unwrap(), SetBehavior::All ); assert_eq!(commands.add[0], SCommand::Simple("cmd1".into())); assert_eq!(commands.sub[0], SCommand::Simple("cmd2".into())); } #[test] fn test_serialize() { let config = SConfig::builder() .role( SRole::builder("role1") .actor(SActor::user("user1").build()) .actor( SActor::group([SGroupType::from("group1"), SGroupType::from(1000)]).build(), ) .task( STask::builder("task1") .purpose("purpose1".into()) .cred( SCredentials::builder() .setuid(SUserEither::UserSelector( SSetuidSet::builder() .fallback("user1") .default(SetBehavior::All) .add(["user2".into()]) .sub(["user3".into()]) .build(), )) .setgid(SGroupsEither::MandatoryGroup(SGroupType::from( "setgid1", ))) .capabilities( SCapabilities::builder(SetBehavior::All) .add_cap(Cap::NET_BIND_SERVICE) .sub_cap(Cap::SYS_ADMIN) .build(), ) .build(), ) .commands( SCommands::builder(SetBehavior::All) .add(["cmd1".into()]) .sub(["cmd2".into()]) .build(), ) .build(), ) .build(), ) .options(|opt| { opt.path( SPathOptions::builder(PathBehavior::Delete) .add(["path_add"]) .sub(["path_sub"]) .build(), ) .env( SEnvOptions::builder(EnvBehavior::Delete) .override_behavior(true) .keep(["keep_env"]) .unwrap() .check(["check_env"]) .unwrap() .build(), ) .root(SPrivileged::Privileged) .bounding(SBounding::Ignore) .authentication(SAuthentication::Skip) .timeout( STimeout::builder() .type_field(TimestampType::PPID) .duration(Duration::minutes(5)) .build(), ) .build() }) .build(); serde_json::to_string_pretty(&config).unwrap(); } #[test] fn test_serialize_operride_behavior_option() { let config = SConfig::builder() .options(|opt| { opt.env( SEnvOptions::builder(EnvBehavior::Inherit) .override_behavior(true) .build(), ) .build() }) .build(); let config = serde_json::to_string(&config).unwrap(); assert_eq!( config, "{\"options\":{\"env\":{\"override_behavior\":true}}}" ); } } rootasrole-core-3.2.0/src/database/versionning.rs000064400000000000000000000014351046102023000202120ustar 00000000000000use semver::Version; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use super::migration::Migration; use crate::{FullSettings, PACKAGE_VERSION}; #[derive(Deserialize, Serialize, Debug)] pub struct Versioning { pub version: Version, #[serde(default, flatten)] pub data: T, } impl Versioning { pub fn new(data: T) -> Self { Self { version: PACKAGE_VERSION.to_owned().parse().unwrap(), data, } } } impl Default for Versioning { fn default() -> Self { Self { version: PACKAGE_VERSION.to_owned().parse().unwrap(), data: T::default(), } } } pub(crate) const SETTINGS_MIGRATIONS: &[Migration] = &[]; rootasrole-core-3.2.0/src/lib.rs000064400000000000000000001411241046102023000146530ustar 00000000000000// Let's define a serde configuration struct to define the database type and connection string // example in json: // { // "storage_method": "sqlite", // storage method is where roles and permissions are stored // "storage_settings": { // "path": "/path/to/sqlite.db" // "host": "localhost", // "port": 5432, // "auth": { // "user": "user", // "password": "password", // "client_ssl": { // "ca_cert": "/path/to/ca_cert", // "client_cert": "/path/to/client_cert", // "client_key": "/path/to/client_key" // } // }, // // when using rdbms as storage method // "database": "database", // "schema": "schema", // "table_prefix": "rar_", // "properties": { // "use_unicode": true, // "character_encoding": "utf8" // }, // // when using ldap as storage method // "role_dn": "ou=roles", // }, // "ldap": { // when using ldap for user and groups definition storage // "enabled": false, // "host": "localhost", // "port": 389, // "auth": { // "user": "user", // "password": "password" // "client_ssl": { // "ca_cert": "/path/to/ca_cert", // "client_cert": "/path/to/client_cert", // "client_key": "/path/to/client_key" // } // }, // "base_dn": "dc=example,dc=com", // "user_dn": "ou=users", // "group_dn": "ou=groups", // "user_filter": "(&(objectClass=person)(sAMAccountName=%s))", // "group_filter": "(&(objectClass=group)(member=%s))" // } // } const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION"); use std::{ cell::RefCell, error::Error, fs::{File, Permissions}, io::{BufReader, Seek}, ops::{Deref, DerefMut}, os::unix::fs::{MetadataExt, PermissionsExt}, path::{Path, PathBuf}, rc::Rc, }; use bon::{builder, Builder}; use capctl::Cap; use libc::dev_t; use log::{debug, warn}; use nix::{ fcntl::Flock, unistd::{getgroups, Gid, Group, Pid, Uid, User}, }; use semver::Version; use serde::{ser::SerializeMap, Deserialize, Serialize}; //pub mod api; pub mod database; //pub mod plugin; pub mod util; use strum::EnumString; use util::{read_with_privileges, write_cbor_config, write_json_config}; use database::{ migration::Migration, structs::SConfig, versionning::{Versioning, SETTINGS_MIGRATIONS}, }; use crate::util::{ has_privileges, is_immutable, open_lock_with_privileges, with_mutable_config, with_privileges, }; #[derive(Debug, Builder)] pub struct Cred { #[builder(field = User::from_uid(Uid::current()).unwrap().unwrap())] pub user: User, #[builder(field = getgroups().unwrap().iter().map(|gid| Group::from_gid(*gid).unwrap().unwrap()) .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, } #[derive(Serialize, Deserialize, Debug, Clone, Builder, Default, PartialEq, Eq)] pub struct RemoteStorageSettings { #[serde(skip_serializing_if = "Option::is_none")] #[builder(name = not_immutable,with = || false)] pub immutable: Option, #[serde(skip_serializing_if = "Option::is_none")] #[builder(into)] pub path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub host: Option, #[serde(skip_serializing_if = "Option::is_none")] pub port: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auth: Option, #[serde(skip_serializing_if = "Option::is_none")] pub database: Option, #[serde(skip_serializing_if = "Option::is_none")] pub schema: Option, #[serde(skip_serializing_if = "Option::is_none")] pub table_prefix: Option, #[serde(skip_serializing_if = "Option::is_none")] pub properties: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ConnectionAuth { pub user: String, #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, #[serde(skip_serializing_if = "Option::is_none")] pub client_ssl: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ClientSsl { pub enabled: bool, #[serde(skip_serializing_if = "Option::is_none")] pub ca_cert: Option, #[serde(skip_serializing_if = "Option::is_none")] pub client_cert: Option, #[serde(skip_serializing_if = "Option::is_none")] 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() { 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)?, ))) } else { None }; 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 { 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(LockedSettingsFile { path: path.as_ref().to_path_buf(), data: load_full_settings(&path, file.deref()) .unwrap_or(Rc::new(RefCell::new(FullSettings::default()))), fd: file, }) }); } } let file = open_lock_with_privileges(path.as_ref(), options, nix::fcntl::FlockArg::LockExclusive)?; Ok(LockedSettingsFile { path: path.as_ref().to_path_buf(), data: load_full_settings(&path, file.deref()) .unwrap_or(Rc::new(RefCell::new(FullSettings::default()))), fd: file, }) } pub fn save(&mut self) -> Result<(), Box> { debug!("Saving settings file: {}", self.path.display()); Migration::migrate( &Version::parse(PACKAGE_VERSION).unwrap(), &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 .as_ref() .unwrap_or(&RemoteStorageSettings::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 { Some(data_path.clone()) } else { None } } 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(self.fd.deref_mut(), |file| { debug!("Toggled immutable off for config file"); file.rewind()?; write_json_config(&versionned, file) })?; } else { let file = self.fd.deref_mut(); 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(()) } 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(file.deref_mut(), |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, self.fd.deref_mut())?; } 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(()) } 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) } 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) } 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) .inspect_err(|e| { debug!("Error reading file: {}", e); }) .unwrap_or_default(), StorageMethod::CBOR => cbor4ii::serde::from_reader(BufReader::new(file)) .inspect_err(|e| { debug!("Error reading file: {}", e); }) .unwrap_or_default(), }; make_weak_config(&value.data); //read_effective(false).or(dac_override_effective(false))?; //assert_eq!(value.version.to_string(), PACKAGE_VERSION, "Version mismatch"); debug!("{}", serde_json::to_string_pretty(&value)?); Ok(value.data) } pub fn migrate_settings(settings: &mut FullSettings) -> Result<(), Box> { Migration::migrate( &Version::parse(PACKAGE_VERSION).unwrap(), settings, SETTINGS_MIGRATIONS, )?; Ok(()) } 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!!"); Default::default() }); //read_effective(false).or(dac_override_effective(false))?; debug!("{}", serde_json::to_string_pretty(&value)?); Ok(value.data) } #[cfg(test)] mod tests { use std::fs; use std::io::{Read, Write}; use crate::database::actor::SActor; use crate::database::structs::{SCommand, SCommands, SCredentials, SRole, STask, SetBehavior}; use super::*; pub struct Defer(Option); impl Defer { pub fn new(f: F) -> Self { Defer(Some(f)) } } impl Drop for Defer { fn drop(&mut self) { if let Some(f) = self.0.take() { f(); } } } pub fn defer(f: F) -> Defer { Defer::new(f) } #[test] fn test_get_settings_same_file() { // Create a test JSON file let value = "/tmp/test_get_settings_same_file.json"; let _cleanup = defer(|| { let filename = PathBuf::from(value).canonicalize().unwrap_or(value.into()); if std::fs::remove_file(&filename).is_err() { debug!("Failed to delete the file: {}", filename.display()); } }); let 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(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(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(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(test_file.into()); if std::fs::remove_file(&filename).is_err() { debug!("Failed to delete the file: {}", filename.display()); } }); let _cleanup2 = defer(|| { let filename = PathBuf::from(external_file) .canonicalize() .unwrap_or(external_file.into()); if std::fs::remove_file(&filename).is_err() { debug!("Failed to delete the file: {}", filename.display()); } }); let sconfig = SConfig::builder() .role( SRole::builder("test_role") .actor(SActor::user(0).build()) .task( STask::builder("test_task") .cred(SCredentials::builder().setuid(0).setgid(0).build()) .commands( SCommands::builder(SetBehavior::None) .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) .build(), ) .build(), ) .build(), ) .build(); // Create a test JSON file let config = Rc::new(RefCell::new( 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(test_file.into()); if std::fs::remove_file(&filename).is_err() { debug!("Failed to delete the file: {}", filename.display()); } }); let _cleanup2 = defer(|| { let filename = PathBuf::from(external_file) .canonicalize() .unwrap_or(external_file.into()); if std::fs::remove_file(&filename).is_err() { debug!("Failed to delete the file: {}", filename.display()); } }); let sconfig = SConfig::builder() .role( SRole::builder("test_role") .actor(SActor::user(0).build()) .task( STask::builder("test_task") .cred(SCredentials::builder().setuid(0).setgid(0).build()) .commands( SCommands::builder(SetBehavior::None) .add(vec![SCommand::Simple("/usr/bin/true".to_string())]) .build(), ) .build(), ) .build(), ) .build(); let settings = Rc::new(RefCell::new( FullSettings::builder() .storage( SettingsContent::builder() .method(StorageMethod::CBOR) .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: settings.clone(), }; 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.to_string(), 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(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(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(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(test_file.into()); if std::fs::remove_file(&filename).is_err() { debug!("Failed to delete the file: {}", filename.display()); } }); let _cleanup2 = defer(|| { let filename = PathBuf::from(external_file) .canonicalize() .unwrap_or(external_file.into()); if std::fs::remove_file(&filename).is_err() { debug!("Failed to delete the file: {}", filename.display()); } }); // 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(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(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(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()); } } rootasrole-core-3.2.0/src/plugin/hashchecker.rs000064400000000000000000000135521046102023000176560ustar 00000000000000use std::{error::Error, fs::File, io::Read, os::fd::AsRawFd}; use crate::{ api::PluginManager, database::structs::SCommand, open_with_privileges, util::{first_path, parse_conf_command}, }; use log::{debug, warn}; use nix::unistd::{access, AccessFlags}; use serde::{Deserialize, Serialize}; use libc::FS_IOC_GETFLAGS; use sha2::Digest; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum HashType { SHA224, SHA256, SHA384, SHA512, } #[derive(Debug, Serialize, Deserialize)] struct HashChecker { hash_type: HashType, hash: String, #[serde(alias = "read-only")] read_only: Option, immutable: Option, command: SCommand, } #[cfg(feature = "finder")] fn compute(hashtype: &HashType, hash: &[u8]) -> Vec { match hashtype { HashType::SHA224 => { let mut hasher = sha2::Sha224::new(); hasher.update(hash); hasher.finalize().to_vec() } HashType::SHA256 => { let mut hasher = sha2::Sha256::new(); hasher.update(hash); hasher.finalize().to_vec() } HashType::SHA384 => { let mut hasher = sha2::Sha384::new(); hasher.update(hash); hasher.finalize().to_vec() } HashType::SHA512 => { let mut hasher = sha2::Sha512::new(); hasher.update(hash); hasher.finalize().to_vec() } } } const FS_IMMUTABLE_FL: u32 = 0x00000010; fn is_immutable(file: &File) -> Result> { let mut val = 0; let fd = file.as_raw_fd(); if unsafe { nix::libc::ioctl(fd, FS_IOC_GETFLAGS, &mut val) } < 0 { debug!("Error getting flags {:?}", std::io::Error::last_os_error()); return Err("Error getting flags".into()); } Ok(val & FS_IMMUTABLE_FL != 0) } fn complex_command_parse( command: &serde_json::Value, ) -> Result, Box> { let checker = serde_json::from_value::(command.clone()); debug!("Checking command {:?}", checker); match checker { Ok(checker) => { process_hash_check(checker) } Err(e) => { debug!("Error parsing command {:?}", e); Err(Box::new(e)) } } } fn process_hash_check(checker: HashChecker) -> Result, Box> { let cmd = parse_conf_command(&checker.command)?; let path = first_path(&cmd[0]).find( |path| access(path, AccessFlags::W_OK).is_ok() ); if path.is_some() { if checker.read_only.is_some_and(|read_only| read_only) { return Err("Executor must not have write access to the executable".into()); } warn!("Executor has write access to the executable, this could lead to a race condition vulnerability"); } if let Some(path) = path { let mut open = open_with_privileges(&path)?; if !is_immutable(&open)? && checker.immutable.is_some_and(|immutable| immutable) { return Err("Executable file must be immutable".into()); } let mut buf = Vec::new(); open.read_to_end(&mut buf)?; let hash = compute(&checker.hash_type, &buf); let config_hash = hex::decode(checker.hash.as_bytes())?; debug!( "Hash: {:?}, Config Hash: {:?}", hex::encode(&hash), hex::encode(&config_hash) ); if hash == config_hash { debug!("Hashes match"); parse_conf_command(&checker.command) } else { debug!("Hashes do not match"); Err("Hashes do not match".into()) } } else { debug!("Path not found"); Err("Path not found".into()) } } pub fn register() { PluginManager::subscribe_complex_command_parser(complex_command_parse) } #[cfg(feature = "finder")] #[cfg(test)] mod tests { use std::{io::Write, rc::Rc}; use nix::unistd::{Pid, User}; use super::*; use crate::database::actor::SActor; //use crate::database::finder::{Cred, TaskMatcher}; use crate::{ database::structs::{IdTask, SCommand, SCommands, SConfig, SRole, STask}, rc_refcell, }; #[test] fn test_plugin_implemented() { register(); // create a file in /tmp let mut file = std::fs::File::create("/tmp/hashchecker").unwrap(); file.write("test".as_bytes()).unwrap(); file.sync_all().unwrap(); let config = rc_refcell!(SConfig::default()); let role1 = rc_refcell!(SRole::default()); role1.as_ref().borrow_mut()._config = Some(Rc::downgrade(&config)); role1.as_ref().borrow_mut().name = "role1".to_string(); let task1 = rc_refcell!(STask::default()); task1.as_ref().borrow_mut()._role = Some(Rc::downgrade(&role1)); task1.as_ref().borrow_mut().name = IdTask::Name("task1".to_string()); let mut command = SCommands::default(); command.add.push(SCommand::Complex(serde_json::json!({ "hash_type": "sha256", "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "command": "/tmp/hashchecker" }))); task1.as_ref().borrow_mut().commands = command; role1.as_ref().borrow_mut().tasks.push(task1); role1 .as_ref() .borrow_mut() .actors .push(SActor::user(0).build()); config.as_ref().borrow_mut().roles.push(role1); let cred = Cred { user: User::from_uid(0.into()).unwrap().unwrap(), groups: vec![], ppid: Pid::parent(), tty: None, }; let matching = config .matches(&cred, &None, &vec!["/tmp/hashchecker".to_string()]) .unwrap(); assert!(matching.score.fully_matching()); std::fs::remove_file("/tmp/hashchecker").unwrap(); } } rootasrole-core-3.2.0/src/plugin/hierarchy.rs000064400000000000000000000160121046102023000173560ustar 00000000000000use std::cmp::Ordering; use crate::{ api::{PluginManager, PluginResultAction}, database::{ finder::{Cred, TaskMatch, TaskMatcher}, structs::{RoleGetter, SRole}, FilterMatcher, }, }; use log::{debug, warn}; use serde::Deserialize; #[derive(Deserialize)] pub struct Parents(Vec); fn get_parents(role: &SRole) -> Option> { role._extra_fields .get("parents") .map(|parents| serde_json::from_value::(parents.clone())) } fn find_in_parents( role: &SRole, user: &Cred, filter: &Option, command: &[String], matcher: &mut TaskMatch, ) -> PluginResultAction { //precondition matcher user matches if !matcher.score.user_matching() { return PluginResultAction::Ignore; } let mut result = PluginResultAction::Ignore; let config = role._config.as_ref().unwrap().upgrade().unwrap(); match get_parents(role) { Some(Ok(parents)) => { debug!("Found parents {:?}", parents.0); for parent in parents.0.iter() { if let Some(role) = config.role(parent) { debug!("Checking parent role {}", parent); match role.as_ref().borrow().tasks.matches(user, filter, command) { Ok(matches) => { debug!("Parent role {} matched", parent); if !matcher.score.command_matching() || (matches.score.command_matching() && matches.score.cmd_cmp(&matcher.score) == Ordering::Less) { debug!("Parent role {} is better", parent); matcher.score.cmd_min = matches.score.cmd_min; matcher.settings = matches.settings; result = PluginResultAction::Edit; } } Err(e) => { warn!("Failed to match parent role {}", e); } } } else { warn!("Parent role {} not found", parent); } } } _ => { warn!("No parents found for role {}", role.name); return PluginResultAction::Ignore; } }; result } pub fn register() { PluginManager::subscribe_role_matcher(find_in_parents); } #[cfg(test)] mod tests { use std::rc::Rc; use nix::unistd::{Pid, User}; use super::*; use crate::{ database::{ actor::SActor, finder::ActorMatchMin, structs::{IdTask, SCommand, SCommands, SConfig, STask}, }, rc_refcell, }; #[test] fn test_find_in_parents() { let config = rc_refcell!(SConfig::default()); let role1 = rc_refcell!(SRole::default()); role1.as_ref().borrow_mut()._config = Some(Rc::downgrade(&config)); role1.as_ref().borrow_mut().name = "role1".to_string(); let task1 = rc_refcell!(STask::default()); task1.as_ref().borrow_mut()._role = Some(Rc::downgrade(&role1)); task1.as_ref().borrow_mut().name = IdTask::Name("task1".to_string()); let mut command = SCommands::default(); command.add.push(SCommand::Simple("ls".to_string())); task1.as_ref().borrow_mut().commands = command; role1.as_ref().borrow_mut().tasks.push(task1); config.as_ref().borrow_mut().roles.push(role1); let role1 = rc_refcell!(SRole::default()); let task1 = rc_refcell!(STask::default()); task1.as_ref().borrow_mut()._role = Some(Rc::downgrade(&role1)); role1.as_ref().borrow_mut()._config = Some(Rc::downgrade(&config)); role1.as_ref().borrow_mut().name = "role2".to_string(); role1 .as_ref() .borrow_mut() .actors .push(SActor::user(0).build()); role1.as_ref().borrow_mut()._extra_fields.insert( "parents".to_string(), serde_json::Value::Array(vec![serde_json::Value::String("role1".to_string())]), ); task1.as_ref().borrow_mut().name = IdTask::Name("task2".to_string()); role1.as_ref().borrow_mut().tasks.push(task1); config.as_ref().borrow_mut().roles.push(role1); let cred = Cred { user: User::from_uid(0.into()).unwrap().unwrap(), groups: vec![], ppid: Pid::parent(), tty: None, }; let mut matcher = TaskMatch::default(); matcher.score.user_min = ActorMatchMin::UserMatch; let res = find_in_parents( &config.as_ref().borrow().roles[1].as_ref().borrow(), &cred, &None, &["ls".to_string()], &mut matcher, ); assert_eq!(res, PluginResultAction::Edit); } #[test] fn test_plugin_implemented() { register(); let config = rc_refcell!(SConfig::default()); let role1 = rc_refcell!(SRole::default()); role1.as_ref().borrow_mut()._config = Some(Rc::downgrade(&config)); role1.as_ref().borrow_mut().name = "role1".to_string(); let task1 = rc_refcell!(STask::default()); task1.as_ref().borrow_mut()._role = Some(Rc::downgrade(&role1)); task1.as_ref().borrow_mut().name = IdTask::Name("task1".to_string()); let mut command = SCommands::default(); command.add.push(SCommand::Simple("ls".to_string())); task1.as_ref().borrow_mut().commands = command; role1.as_ref().borrow_mut().tasks.push(task1); config.as_ref().borrow_mut().roles.push(role1); let role1 = rc_refcell!(SRole::default()); let task1 = rc_refcell!(STask::default()); task1.as_ref().borrow_mut()._role = Some(Rc::downgrade(&role1)); role1.as_ref().borrow_mut()._config = Some(Rc::downgrade(&config)); role1.as_ref().borrow_mut().name = "role2".to_string(); role1 .as_ref() .borrow_mut() .actors .push(SActor::user(0).build()); role1.as_ref().borrow_mut()._extra_fields.insert( "parents".to_string(), serde_json::Value::Array(vec![serde_json::Value::String("role1".to_string())]), ); task1.as_ref().borrow_mut().name = IdTask::Name("task2".to_string()); role1.as_ref().borrow_mut().tasks.push(task1); config.as_ref().borrow_mut().roles.push(role1); let cred = Cred { user: User::from_uid(0.into()).unwrap().unwrap(), groups: vec![], ppid: Pid::parent(), tty: None, }; let mut matcher = TaskMatch::default(); matcher.score.user_min = ActorMatchMin::UserMatch; let matches = config.matches(&cred, &None, &["ls".to_string()]).unwrap(); assert_eq!( matches.settings.task.upgrade().unwrap(), config.as_ref().borrow().roles[0].as_ref().borrow().tasks[0].clone() ); } } rootasrole-core-3.2.0/src/plugin/mod.rs000064400000000000000000000005031046102023000161550ustar 00000000000000#[cfg(feature = "finder")] mod hashchecker; #[cfg(feature = "finder")] mod hierarchy; #[cfg(feature = "finder")] mod ssd; pub fn register_plugins() { #[cfg(feature = "finder")] hashchecker::register(); #[cfg(feature = "finder")] ssd::register(); #[cfg(feature = "finder")] hierarchy::register(); } rootasrole-core-3.2.0/src/plugin/ssd.rs000064400000000000000000000150071046102023000161740ustar 00000000000000use std::{cell::RefCell, ffi::CString, rc::Rc}; use ::serde::Deserialize; use nix::unistd::{getgrouplist, Group, User}; use serde_json::Error; use crate::{ api::{PluginManager, PluginResult}, as_borrow, database::{ actor::{SActor, SGroups}, finder::Cred, structs::{RoleGetter, SConfig, SRole}, }, }; #[derive(Deserialize)] pub struct Ssd(Vec); fn user_contained_in(user: &User, actors: &[SActor]) -> bool { for actor in actors.iter() { if let SActor::User { id, .. } = actor { if let Some(id) = id { if id == user { return true; } } else { //TODO API call to verify if user is the described actor return false; } } } false } fn group_contained_in(pgroup: &Group, actors: &[SActor]) -> bool { for actor in actors.iter() { if let SActor::Group { groups, .. } = actor { if let Some(groups) = groups { match groups { SGroups::Single(group) => { if group == pgroup { return true; } } SGroups::Multiple(groups) => { if groups.iter().any(|x| x == pgroup) { return true; } } } } else { //TODO API call to verify if group is the described actor return false; } } } false } fn groups_subset_of(groups: &[Group], actors: &[SActor]) -> bool { for group in groups.iter() { if !group_contained_in(group, actors) { return false; } } true } // Check if user and its related groups are forbidden to use the role fn user_is_forbidden(user: &User, ssd_roles: &[String], sconfig: Rc>) -> bool { let mut groups_to_check = Vec::new(); if let Ok(groups) = getgrouplist( CString::new(user.name.as_str()).unwrap().as_c_str(), user.gid, ) { for group in groups.iter() { let group = nix::unistd::Group::from_gid(group.to_owned()); if let Ok(Some(group)) = group { groups_to_check.push(group); } } } for role in ssd_roles.iter() { if let Some(role) = sconfig.role(role) { if user_contained_in(user, &as_borrow!(role).actors) || groups_subset_of(&groups_to_check, &as_borrow!(role).actors) { return true; } } } false } fn groups_are_forbidden( groups: &[Group], ssd_roles: &[String], sconfig: Rc>, ) -> bool { for role in ssd_roles.iter() { if let Some(role) = sconfig.role(role) { if groups_subset_of(groups, &as_borrow!(role).actors) { return true; } } } false } fn check_separation_of_duty(role: &SRole, actor: &Cred) -> PluginResult { let ssd = get_ssd_entry(role); if ssd.is_none() { return PluginResult::Neutral; } let sconfig = role ._config .as_ref() .expect("role should have its config") .upgrade() .expect("internal error"); let roles = ssd.unwrap(); if roles.is_err() { return PluginResult::Neutral; } let roles = roles.unwrap().0; if user_is_forbidden(&actor.user, &roles, sconfig.clone()) || groups_are_forbidden(&actor.groups, &roles, sconfig.clone()) { PluginResult::Deny } else { PluginResult::Neutral } } fn get_ssd_entry(role: &SRole) -> Option> { role._extra_fields .get("ssd") .map(|ssd| serde_json::from_value::(ssd.clone())) } pub fn register() { PluginManager::subscribe_duty_separation(check_separation_of_duty) } #[cfg(test)] mod tests { use std::rc::Rc; use super::*; use crate::{ database::structs::{SConfig, SRole}, rc_refcell, }; use nix::unistd::{Group, Pid}; use serde_json::Value; #[test] fn test_user_contained_in() { let user = User::from_uid(0.into()).unwrap().unwrap(); let actors = vec![SActor::user(0).build()]; assert!(user_contained_in(&user, &actors)); } #[test] fn test_group_contained_in() { let group = Group::from_gid(0.into()).unwrap().unwrap(); let actors = vec![SActor::group(0).build()]; assert!(group_contained_in(&group, &actors)); } #[test] fn test_groups_subset_of() { let groups = vec![Group::from_gid(0.into()).unwrap().unwrap()]; let actors = vec![SActor::group(0).build()]; assert!(groups_subset_of(&groups, &actors)); } #[test] fn test_user_is_forbidden() { let user = User::from_uid(0.into()).unwrap().unwrap(); let sconfig = SConfig::builder().build(); let roles = vec!["role1".to_string()]; assert!(!user_is_forbidden(&user, &roles, sconfig)); } #[test] fn test_groups_are_forbidden() { let groups = vec![Group::from_gid(0.into()).unwrap().unwrap()]; let sconfig = SConfig::builder() .role( SRole::builder("role1".to_string()) .actor(SActor::group(0).build()) .build(), ) .build(); let roles = vec!["role1".to_string()]; assert!(groups_are_forbidden(&groups, &roles, sconfig)); } #[test] fn test_check_separation_of_duty() { let sconfig = rc_refcell!(SConfig::default()); let role = rc_refcell!(SRole::default()); role.as_ref().borrow_mut()._config = Some(Rc::downgrade(&sconfig)); role.as_ref().borrow_mut().name = "role1".to_string(); role.as_ref() .borrow_mut() .actors .push(SActor::group(0).build()); role.as_ref().borrow_mut()._extra_fields.insert( "ssd".to_string(), serde_json::Value::Array(vec![Value::String("role1".to_string())]), ); sconfig.as_ref().borrow_mut().roles.push(role.clone()); let actor = Cred { user: User::from_uid(0.into()).unwrap().unwrap(), groups: vec![Group::from_gid(0.into()).unwrap().unwrap()], tty: None, ppid: Pid::parent(), }; assert_eq!( check_separation_of_duty(&role.as_ref().borrow(), &actor), PluginResult::Deny ); } } rootasrole-core-3.2.0/src/util.rs000064400000000000000000000430641046102023000150660ustar 00000000000000use std::{ fs::{File, OpenOptions}, io::{self, ErrorKind, Write}, os::{fd::AsRawFd, unix::fs::MetadataExt}, path::{Path, PathBuf}, }; use capctl::{prctl, CapState}; use capctl::{Cap, CapSet, ParseCapError}; use libc::{FS_IOC_GETFLAGS, FS_IOC_SETFLAGS}; use log::{debug, warn}; use nix::fcntl::{Flock, FlockArg}; use serde::Serialize; #[cfg(feature = "finder")] use crate::database::score::CmdMin; pub const RST: &str = "\x1B[0m"; pub const BOLD: &str = "\x1B[1m"; pub const UNDERLINE: &str = "\x1B[4m"; pub const RED: &str = "\x1B[31m"; // Hardened enum values used for critical enums to mitigate attacks like Rowhammer. // See for example https://arxiv.org/pdf/2309.02545.pdf // The values are copied from https://github.com/sudo-project/sudo/commit/7873f8334c8d31031f8cfa83bd97ac6029309e4f#diff-b8ac7ab4c3c4a75aed0bb5f7c5fd38b9ea6c81b7557f775e46c6f8aa115e02cd pub const HARDENED_ENUM_VALUE_0: u32 = 0x052a2925; // 0101001010100010100100100101 pub const HARDENED_ENUM_VALUE_1: u32 = 0x0ad5d6da; // 1010110101011101011011011010 pub const HARDENED_ENUM_VALUE_2: u32 = 0x69d61fc8; // 1101001110101100001111111001000 pub const HARDENED_ENUM_VALUE_3: u32 = 0x1629e037; // 0010110001010011110000000110111 pub const HARDENED_ENUM_VALUE_4: u32 = 0x1fc8d3ac; // 11111110010001101001110101100 #[macro_export] macro_rules! upweak { ($e:expr) => { $e.upgrade().unwrap() }; } #[macro_export] macro_rules! as_borrow { ($e:expr) => { $e.as_ref().borrow() }; } #[macro_export] macro_rules! as_borrow_mut { ($e:expr) => { $e.as_ref().borrow_mut() }; } #[macro_export] macro_rules! rc_refcell { ($e:expr) => { std::rc::Rc::new(std::cell::RefCell::new($e)) }; } const FS_IMMUTABLE_FL: u32 = 0x00000010; pub fn immutable_required_privileges(file: &File, f: F) -> std::io::Result where F: FnOnce() -> std::io::Result, { let metadata = file.metadata()?; let uid = metadata.uid(); let gid = metadata.gid(); let effective_uid = nix::unistd::Uid::effective(); let effective_gid = nix::unistd::Gid::effective(); let caps = if effective_uid != nix::unistd::Uid::from_raw(uid) && effective_gid != nix::unistd::Gid::from_raw(gid) { vec![Cap::LINUX_IMMUTABLE, Cap::FOWNER, Cap::DAC_OVERRIDE] } else { vec![Cap::LINUX_IMMUTABLE] }; with_privileges(&caps, f) } pub(crate) fn is_immutable(file: &File) -> std::io::Result { let mut val = 0; if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_GETFLAGS, &mut val) } < 0 { return Err(std::io::Error::last_os_error()); } Ok(val & FS_IMMUTABLE_FL != 0) } /// Perform a writing operation on a writable opened file descriptor with the immutable flag set /// The function will temporarily remove the immutable flag, perform the operation and set it back pub fn with_mutable_config(file: &mut File, f: F) -> std::io::Result where F: FnOnce(&mut File) -> io::Result, { let mut val = 0; if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_GETFLAGS, &mut val) } < 0 { return Err(std::io::Error::last_os_error()); } if val & FS_IMMUTABLE_FL != 0 { val &= !(FS_IMMUTABLE_FL); immutable_required_privileges(file, || { if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_SETFLAGS, &mut val) } < 0 { return Err(std::io::Error::last_os_error()); } Ok(()) })?; } else { warn!("Config file was not immutable."); } let res = f(file); val |= FS_IMMUTABLE_FL; immutable_required_privileges(file, || { if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_SETFLAGS, &mut val) } < 0 { return Err(std::io::Error::last_os_error()); } Ok(()) })?; res } pub fn warn_if_mutable(file: &File, return_err: bool) -> std::io::Result<()> { let mut val = 0; let fd = file.as_raw_fd(); if unsafe { nix::libc::ioctl(fd, FS_IOC_GETFLAGS, &mut val) } < 0 { return Err(std::io::Error::last_os_error()); } if val & FS_IMMUTABLE_FL == 0 { if return_err { return Err(std::io::Error::new( ErrorKind::ReadOnlyFilesystem, "Config file is not immutable, ask your administrator to solve this issue", )); } warn!("Config file is not immutable, think about setting the immutable flag."); } Ok(()) } //parse string iterator to capset pub fn parse_capset_iter<'a, I>(iter: I) -> Result where I: Iterator, { let mut res = CapSet::empty(); for part in iter { match part.parse() { Ok(cap) => res.add(cap), Err(error) => { return Err(error); } } } Ok(res) } /// Reference every capabilities that lead to almost a direct privilege escalation pub fn capabilities_are_exploitable(caps: &CapSet) -> bool { caps.has(Cap::SYS_ADMIN) || caps.has(Cap::SYS_PTRACE) || caps.has(Cap::SYS_MODULE) || caps.has(Cap::DAC_READ_SEARCH) || caps.has(Cap::DAC_OVERRIDE) || caps.has(Cap::FOWNER) || caps.has(Cap::CHOWN) || caps.has(Cap::SETUID) || caps.has(Cap::SETGID) || caps.has(Cap::SETFCAP) || caps.has(Cap::SYS_RAWIO) || caps.has(Cap::LINUX_IMMUTABLE) || caps.has(Cap::SYS_CHROOT) || caps.has(Cap::SYS_BOOT) || caps.has(Cap::MKNOD) } pub fn definitive_drop(needed: &[Cap]) -> Result<(), capctl::Error> { let capset = !CapSet::from_iter(needed.iter().cloned()); capctl::ambient::clear()?; let mut current = CapState::get_current()?; current.permitted -= capset; current.inheritable.clear(); current.effective.clear(); current.set_current()?; Ok(()) } pub fn escape_parser_string(s: S) -> String where S: AsRef, { remove_outer_quotes(s.as_ref()) } fn remove_outer_quotes(input: &str) -> String { if input.len() >= 2 && (input.starts_with('"') && input.ends_with('"') || input.starts_with('\'') && input.ends_with('\'')) { remove_outer_quotes(&input[1..input.len() - 1]) } else { input.to_string() } } pub fn all_paths_from_env>(env_path: &[&str], exe_name: P) -> Vec { env_path .iter() .filter_map(|dir| { let full_path = Path::new(dir).join(&exe_name); debug!("Checking path: {:?}", full_path); full_path.is_file().then_some(full_path) }) .collect() } #[cfg(feature = "finder")] pub fn match_single_path(cmd_path: &PathBuf, role_path: &str) -> CmdMin { if !role_path.ends_with(cmd_path.to_str().unwrap()) || !role_path.starts_with("/") { // the files could not be the same return CmdMin::default(); } let mut match_status = CmdMin::default(); debug!("Matching path {:?} with {:?}", cmd_path, role_path); if cmd_path == Path::new(role_path) { match_status.set_matching(); } else { #[cfg(feature = "glob")] { use glob::Pattern; if let Ok(pattern) = Pattern::new(role_path) { if pattern.matches_path(&cmd_path) { use crate::database::score::CmdOrder; match_status.union_order(CmdOrder::WildcardPath); } } } } if !match_status.matching() { debug!( "No match for path ``{:?}`` for evaluated path : ``{:?}``", cmd_path, role_path ); } match_status } #[cfg(debug_assertions)] pub fn subsribe(_: &str) -> io::Result<()> { env_logger::Builder::from_default_env() .filter_level(log::LevelFilter::Debug) .format_module_path(true) .init(); Ok(()) } #[cfg(not(debug_assertions))] pub fn subsribe(tool: &str) -> io::Result<()> { use log::LevelFilter; use syslog::Facility; syslog::init(Facility::LOG_AUTH, LevelFilter::Info, Some(tool)).map_err(|e| { io::Error::new( io::ErrorKind::Other, format!("Failed to connect to syslog: {}", e), ) })?; Ok(()) } pub fn drop_effective() -> Result<(), capctl::Error> { stated_drop_effective(CapState::get_current()?) } pub fn stated_drop_effective(mut current: CapState) -> Result<(), capctl::Error> { current.effective.clear(); current.set_current() } pub fn initialize_capabilities(cap: &[Cap]) -> Result { let mut current = CapState::get_current()?; current.effective.add_all(cap.iter().cloned()); current .set_current() .inspect_err(|e| debug!("initialize_capabilities error: {}", e))?; Ok(current) } pub fn with_privileges(cap: &[Cap], f: F) -> std::io::Result where F: FnOnce() -> std::io::Result, { let state = initialize_capabilities(cap)?; let res = f(); stated_drop_effective(state)?; res } pub fn has_privileges(cap: &[Cap]) -> Result { let current = CapState::get_current()?; Ok(cap.iter().all(|c| current.permitted.has(*c))) } pub fn activates_no_new_privs() -> Result<(), capctl::Error> { prctl::set_no_new_privs() } pub fn write_json_config(settings: &T, file: &mut impl Write) -> std::io::Result<()> { serde_json::to_writer_pretty(file, &settings)?; Ok(()) } pub 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::new( std::io::ErrorKind::Other, format!("Failed to write cbor config: {}", e), ) }) } pub fn create_with_privileges>(p: P) -> std::io::Result { std::fs::File::create(&p).or_else(|e| { if e.kind() != std::io::ErrorKind::PermissionDenied { return Err(e); } with_privileges(&[Cap::DAC_OVERRIDE], || std::fs::File::create(p)) }) } pub fn open_lock_with_privileges>( p: P, options: OpenOptions, lock: FlockArg, ) -> std::io::Result> { options .open(&p) .or_else(|e| { if e.kind() != std::io::ErrorKind::PermissionDenied { return Err(e); } debug!("Permission denied while opening file, retrying with privileges",); with_privileges(&[Cap::DAC_READ_SEARCH], || options.open(&p)).or_else(|e| { if e.kind() != std::io::ErrorKind::PermissionDenied { return Err(e); } with_privileges(&[Cap::DAC_OVERRIDE], || options.open(&p)) }) }) .and_then(|file| Ok(nix::fcntl::Flock::lock(file, lock).map_err(|(_, e)| e)?)) } pub fn read_with_privileges>(p: P) -> std::io::Result { debug!("Opening file {:?}", p.as_ref()); std::fs::File::open(&p).or_else(|e| { if e.kind() != std::io::ErrorKind::PermissionDenied { return Err(e); } debug!("Permission denied while opening file, retrying with privileges",); with_privileges(&[Cap::DAC_READ_SEARCH], || std::fs::File::open(&p)).or_else(|e| { if e.kind() != std::io::ErrorKind::PermissionDenied { return Err(e); } with_privileges(&[Cap::DAC_OVERRIDE], || std::fs::File::open(&p)) }) }) } pub fn remove_with_privileges>(p: P) -> std::io::Result<()> { std::fs::remove_file(&p).or_else(|e| { if e.kind() != std::io::ErrorKind::PermissionDenied { return Err(e); } debug!("Permission denied while removing file, retrying with privileges",); with_privileges(&[Cap::DAC_OVERRIDE], || std::fs::remove_file(&p)) }) } pub fn create_dir_all_with_privileges>(p: P) -> std::io::Result<()> { std::fs::create_dir_all(&p).or_else(|e| { if e.kind() != std::io::ErrorKind::PermissionDenied { return Err(e); } debug!("Permission denied while creating directory, retrying with privileges",); with_privileges(&[Cap::DAC_OVERRIDE], || std::fs::create_dir_all(p)) }) } #[cfg(test)] mod test { use std::{ fs, io::{ErrorKind, Write}, }; use super::*; pub struct Defer(Option); impl Defer { pub fn new(f: F) -> Self { Defer(Some(f)) } } impl Drop for Defer { fn drop(&mut self) { if let Some(f) = self.0.take() { f(); } } } pub fn defer(f: F) -> Defer { Defer::new(f) } #[test] fn test_remove_outer_quotes() { assert_eq!(remove_outer_quotes("'test'"), "test"); assert_eq!(remove_outer_quotes("\"test\""), "test"); assert_eq!(remove_outer_quotes("test"), "test"); assert_eq!(remove_outer_quotes("t'est"), "t'est"); assert_eq!(remove_outer_quotes("t\"est"), "t\"est"); } #[test] fn test_parse_capset_iter() { let capset = parse_capset_iter( vec!["CAP_SYS_ADMIN", "CAP_SYS_PTRACE", "CAP_DAC_READ_SEARCH"].into_iter(), ) .expect("Failed to parse capset"); assert!(capset.has(Cap::SYS_ADMIN)); assert!(capset.has(Cap::SYS_PTRACE)); assert!(capset.has(Cap::DAC_READ_SEARCH)); } #[test] fn test_capabilities_are_exploitable() { let mut capset = CapSet::empty(); capset.add(Cap::SYS_ADMIN); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::SYS_PTRACE); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::SYS_MODULE); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::DAC_READ_SEARCH); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::DAC_OVERRIDE); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::FOWNER); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::CHOWN); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::SETUID); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::SETGID); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::SETFCAP); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::SYS_RAWIO); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::LINUX_IMMUTABLE); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::SYS_CHROOT); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::SYS_BOOT); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::MKNOD); assert!(capabilities_are_exploitable(&capset)); capset.clear(); capset.add(Cap::WAKE_ALARM); assert!(!capabilities_are_exploitable(&capset)); } #[test] fn test_with_mutable_config() { let current = CapState::get_current().expect("Failed to get current capabilities"); if !current.permitted.has(Cap::LINUX_IMMUTABLE) { eprintln!("Skipping test, requires CAP_LINUX_IMMUTABLE"); return; } let path = PathBuf::from("/tmp/rar_test_lock_config.lock"); let mut file = File::create(&path).expect("Failed to create file"); let _defer = defer(|| { if fs::remove_file(&path).is_err() { // remove the immutable flag if set with_privileges(&[Cap::LINUX_IMMUTABLE], || { let file = File::open(&path).expect("Failed to open file"); let mut val = 0; if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_GETFLAGS, &mut val) } < 0 { eprintln!("Failed to get flags"); return Err(std::io::Error::last_os_error()); } if val & FS_IMMUTABLE_FL != 0 { val &= !(FS_IMMUTABLE_FL); immutable_required_privileges(&file, || { if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_SETFLAGS, &mut val) } < 0 { eprintln!("Failed to remove immutable flag"); } Ok(()) }) .ok(); } fs::remove_file(&path) }).unwrap(); } }); assert!(with_privileges(&[Cap::LINUX_IMMUTABLE], || { let mut val = 0; assert!(unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_GETFLAGS, &mut val) } == 0); val |= FS_IMMUTABLE_FL; immutable_required_privileges(&file, || { if unsafe { nix::libc::ioctl(file.as_raw_fd(), FS_IOC_SETFLAGS, &mut val) } < 0 { return Err(std::io::Error::last_os_error()); } Ok(()) }) }) .and_then(|_| { assert_eq!( File::create(&path).unwrap_err().kind(), ErrorKind::PermissionDenied ); with_mutable_config(&mut file, |file| { file.write_all(b"Test content")?; Ok(()) }) }) .is_ok()); } }