commit 5dc4a8364eb2baa89f76e1e9444f30a458513738 Author: Red Bear OS Date: Sat Jun 27 09:21:43 2026 +0300 Red Bear OS userutils baseline from 0.1.0 pre-patched archive diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..b83d22266a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..63af98548a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: rust +rust: + - nightly +sudo: false +notifications: + email: false diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000..b43cd4a762 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,665 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "extra" +version = "0.1.0" +source = "git+https://gitlab.redox-os.org/redox-os/libextra.git#cf213969493db8667052a591e32a1e26d43c4234" + +[[package]] +name = "generic-rt" +version = "0.1.0" +source = "git+https://gitlab.redox-os.org/redox-os/relibc#3a8f64aa143184dc11fe1927a4f9e55c33ae3052" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "goblin" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27c1b4369c2cd341b5de549380158b105a04c331be5db9110eef7b6d2742134" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ioslice" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e571352c8a3b89074d12e3ee5173ffe162159105352aaaf1fc5764da747e31b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[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.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.9.4", + "ioslice", + "libc", + "redox_syscall", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "numtoa" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libc", + "libredox", + "sdl2", + "sdl2-sys", +] + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox-path" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436d45c2b6a5b159d43da708e62b25be3a4a3d5550d654b72216ade4c4bfd717" + +[[package]] +name = "redox-rt" +version = "0.1.0" +source = "git+https://gitlab.redox-os.org/redox-os/relibc#3a8f64aa143184dc11fe1927a4f9e55c33ae3052" +dependencies = [ + "bitflags 2.9.4", + "generic-rt", + "goblin", + "ioslice", + "plain", + "redox-path", + "redox_syscall", +] + +[[package]] +name = "redox-scheme" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb0cee524a5c6e4db180250f699e101ee25f3f79ad2618527e539e028cb6722" +dependencies = [ + "libredox", + "redox_syscall", +] + +[[package]] +name = "redox_event" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c30b73c93693667b5a8e6f54bddcc5a57449ef17c99f4b55c12d5bae4ab79e82" +dependencies = [ + "bitflags 2.9.4", + "libredox", +] + +[[package]] +name = "redox_liner" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4596948a78a2ac29268d4ee45f788789ac8c032c77cf255378e5c96ca0c779" +dependencies = [ + "bytecount", + "itertools", + "strip-ansi-escapes", + "termion", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.9.4", +] + +[[package]] +name = "redox_termios" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "rust-argon2", + "thiserror", + "zeroize", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sdl2" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" +dependencies = [ + "cfg-if", + "libc", + "version-compare", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[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 = "termion" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3669a69de26799d6321a5aa713f55f7e2cd37bd47be044b50f2acafc42c122bb" +dependencies = [ + "libc", + "libredox", + "numtoa", + "redox_termios", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "userutils" +version = "0.1.0" +dependencies = [ + "clap", + "extra", + "ioslice", + "libc", + "libredox", + "orbclient", + "plain", + "redox-rt", + "redox-scheme", + "redox_event", + "redox_liner", + "redox_syscall", + "redox_termios", + "redox_users", + "serde", + "termion", + "toml", +] + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[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 = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..abfc7aedb9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "userutils" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "id" +path = "src/bin/id.rs" + +[[bin]] +name = "getty" +path = "src/bin/getty.rs" + +[[bin]] +name = "groupadd" +path = "src/bin/groupadd.rs" + +[[bin]] +name = "groupdel" +path = "src/bin/groupdel.rs" + +[[bin]] +name = "groupmod" +path = "src/bin/groupmod.rs" + +[[bin]] +name = "login" +path = "src/bin/login.rs" + +[[bin]] +name = "passwd" +path = "src/bin/passwd.rs" + +[[bin]] +name = "su" +path = "src/bin/su.rs" + +[[bin]] +name = "sudo" +path = "src/bin/sudo.rs" + +[[bin]] +name = "useradd" +path = "src/bin/useradd.rs" + +[[bin]] +name = "userdel" +path = "src/bin/userdel.rs" + +[[bin]] +name = "usermod" +path = "src/bin/usermod.rs" + +[dependencies] +clap = "2.33.0" +extra = { git = "https://gitlab.redox-os.org/redox-os/libextra.git" } +orbclient = "0.3.47" +plain = "0.2.3" +redox_liner = "0.5.2" +libredox = { version = "0.1.12", features = ["mkns"] } +redox_termios = "0.1.3" +redox_event = "0.4.3" +redox-scheme = "0.9.0" +redox_syscall = "0.7.0" +redox_users = "0.4.6" +termion = "4" +libc = "0.2" +serde = { version = "1.0.203", features = ["derive"] } +toml = "0.8.11" +ioslice = "0.6" + +[target.'cfg(target_os = "redox")'.dependencies] +redox-rt = { git = "https://gitlab.redox-os.org/redox-os/relibc", default-features = false } + +[profile.release] +lto = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..aec355d673 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 The Redox developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..e7ade1db59 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Redox OS user and group utilities. + +The `userutils` crate contains the utilities for dealing with users and groups in Redox OS. +They are heavily influenced by UNIX and are, when needed, tailored to specific Redox use cases. + +These implementations strive to be as simple as possible drawing particular +inspiration by BSD systems. They are indeed small, by choice. + +[![Travis Build Status](https://travis-ci.org/redox-os/userutils.svg?branch=master)](https://travis-ci.org/redox-os/userutils) + +**Currently included:** + +- `getty`: Used by `init(8)` to open and initialize the TTY line, read a login name and invoke `login(1)`. +- `id`: Displays user identity. +- `login`: Allows users to login into the system +- `passwd`: Allows users to modify their passwords. +- `su`: Allows users to substitute identity. +- `sudo`: Enables users to execute a command as another user. +- `useradd`: Add a user +- `usermod`: Modify user information +- `userdel`: Delete a user +- `groupadd`: Add a user group +- `groupmod`: Modify group information +- `groupdel`: Remove a user group diff --git a/res/issue b/res/issue new file mode 100644 index 0000000000..59ffbd42fa --- /dev/null +++ b/res/issue @@ -0,0 +1,6 @@ +########## Red Bear OS ######### +# Login with the following: # +# `user` # +# `root`:`password` # +################################ + diff --git a/res/issue.orig b/res/issue.orig new file mode 100644 index 0000000000..6a963d8a70 --- /dev/null +++ b/res/issue.orig @@ -0,0 +1,6 @@ +########## Redox OS ########## +# Login with the following: # +# `user` # +# `root`:`password` # +############################## + diff --git a/res/motd b/res/motd new file mode 100644 index 0000000000..dc28b04cc2 --- /dev/null +++ b/res/motd @@ -0,0 +1,2 @@ +Welcome to Red Bear OS! + diff --git a/src/bin/getty.rs b/src/bin/getty.rs new file mode 100644 index 0000000000..455ee7ad47 --- /dev/null +++ b/src/bin/getty.rs @@ -0,0 +1,285 @@ +#[macro_use] +extern crate clap; + +use std::error::Error; +use std::fs::File; +use std::io::{self, ErrorKind, Read, Stderr, Write}; +use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; +use std::process::{Child, Command, Stdio}; +use std::str; +use std::time::{Duration, Instant}; + +use event::{EventFlags, RawEventQueue}; +use extra::io::fail; +use libredox::call as redox; +use libredox::errno::EAGAIN; +use libredox::flag; + +const _MAN_PAGE: &'static str = /* @MANSTART{getty} */ + r#" +NAME + getty - set terminal mode + +SYNOPSIS + getty [-J | --noclear | -C | --contain ] tty + getty [ -h | --help ] + +DESCRIPTION + The getty utility is called by init(8) to open and initialize the tty line, + read a login name, and invoke login(1). + +OPTIONS + + -h, --help + Display this help and exit. + + -J, --noclear + Do not clear the screen before forking login(1). + + -C, --contain + Run contain_login instead of login + +AUTHOR + Written by Jeremy Soller. +"#; /* @MANEND */ + +const DEFAULT_COLS: u16 = 80; +const DEFAULT_LINES: u16 = 30; + +pub fn handle( + event_queue: &mut RawEventQueue, + tty_fd: RawFd, + master_fd: RawFd, + process: &mut Child, +) { + // tty_fd => Display + // master_fd => PTY + + let handle_event = |event_id: usize| { + if event_id as RawFd == tty_fd { + let mut packet = [0; 4096]; + loop { + let count = match redox::read(tty_fd as usize, &mut packet) { + Ok(0) => return, + Ok(count) => count, + Err(ref err) if err.errno() == EAGAIN => break, + Err(_) => panic!("getty: failed to read from TTY"), + }; + redox::write(master_fd as usize, &packet[..count]) + .expect("getty: failed to write master PTY"); + } + } else if event_id as RawFd == master_fd { + let mut packet = [0; 4096]; + loop { + let count = match redox::read(master_fd as usize, &mut packet) { + Ok(0) => return, + Ok(count) => count, + Err(ref err) if err.errno() == EAGAIN => break, + Err(_) => panic!("getty: failed to read from master TTY"), + }; + redox::write(tty_fd as usize, &packet[1..count]) + .expect("getty: failed to write to TTY"); + if packet[0] & 1 == 1 { + let _ = redox::fsync(tty_fd as usize); + } + } + } + }; + + handle_event(tty_fd as usize); + handle_event(master_fd as usize); + + 'events: loop { + let sys_event = event_queue + .next() + .expect("getty: event queue stopped") + .expect("getty: failed to read event file"); + handle_event(sys_event.fd); + + match process.try_wait() { + Ok(status) => match status { + Some(_code) => break 'events, + None => (), + }, + Err(err) => match err.kind() { + ErrorKind::WouldBlock => (), + _ => panic!("getty: failed to wait on child: {:?}", err), + }, + } + } + + let _ = process.kill(); + process.wait().expect("getty: failed to wait on login"); +} + +pub fn getpty(columns: u16, lines: u16) -> (RawFd, String) { + let master = redox::open( + "/scheme/pty", + flag::O_CLOEXEC | flag::O_RDWR | flag::O_CREAT | flag::O_NONBLOCK, + 0, + ) + .expect("getty: failed to create PTY"); + + if let Ok(winsize_fd) = redox::dup(master, b"winsize") { + let _ = redox::write( + winsize_fd, + &redox_termios::Winsize { + ws_row: lines, + ws_col: columns, + }, + ); + let _ = redox::close(winsize_fd); + } + + let mut buf: [u8; 4096] = [0; 4096]; + let count = redox::fpath(master, &mut buf).unwrap(); + (master as RawFd, unsafe { + String::from_utf8_unchecked(Vec::from(&buf[..count])) + }) +} + +// termion cursor_pos prone to error and does not work on nonblocking files +fn tty_cursor_pos(tty: &mut File) -> Result<(u16, u16), Box> { + write!(tty, "\x1B[6n")?; + tty.flush()?; + + let timeout = Duration::from_millis(500); + let instant = Instant::now(); + let mut data = String::new(); + while instant.elapsed() < timeout { + let mut bytes = [0]; + match tty.read(&mut bytes) { + Ok(count) => if count == 1 { + let c = bytes[0] as char; + if c == 'R' { + break; + } + data.push(c); + }, + Err(err) => if err.kind() != ErrorKind::WouldBlock { + return Err(err.into()); + } + } + } + + if data.is_empty() { + return Err("cursor position timed out".into()); + } + + let beg = data.rfind('[').ok_or("failed to find [")?; + let coords: String = data.chars().skip(beg + 1).collect(); + let mut nums = coords.split(';'); + + let row = nums.next().ok_or("failed to find row")?.parse::()?; + let col = nums.next().ok_or("failed to find col")?.parse::()?; + + Ok((col, row)) +} + +fn tty_columns_lines(tty: &mut File) -> Result<(u16, u16), Box> { + write!(tty, "{}", termion::cursor::Save)?; + tty.flush()?; + + write!(tty, "{}", termion::cursor::Goto(999, 999))?; + tty.flush()?; + + let res = tty_cursor_pos(tty); + + write!(tty, "{}", termion::cursor::Restore)?; + tty.flush()?; + + res +} + +fn daemon(tty: &mut File, clear: bool, contain: bool, stderr: &mut Stderr) { + let (columns, lines) = tty_columns_lines(tty).unwrap_or((DEFAULT_COLS, DEFAULT_LINES)); + let tty_fd = tty.as_raw_fd(); + + let (master_fd, pty) = getpty(columns, lines); + + let mut event_queue = event::RawEventQueue::new().expect("getty: failed to open event queue"); + + event_queue + .subscribe(tty_fd as usize, 0, EventFlags::READ) + .expect("getty: failed to fevent TTY"); + + event_queue + .subscribe(master_fd as usize, 0, EventFlags::READ) + .expect("getty: failed to fevent master PTY"); + + loop { + if clear { + let _ = redox::write(tty_fd as usize, b"\x1Bc"); + } + let _ = redox::fsync(tty_fd as usize); + + let slave_stdin = redox::open(&pty, flag::O_CLOEXEC | flag::O_RDONLY, 0) + .expect("getty: failed to open slave stdin"); + let slave_stdout = redox::open(&pty, flag::O_CLOEXEC | flag::O_WRONLY, 0) + .expect("getty: failed to open slave stdout"); + let slave_stderr = redox::open(&pty, flag::O_CLOEXEC | flag::O_WRONLY, 0) + .expect("getty: failed to open slave stderr"); + + let mut command = if contain { + Command::new("contain_login") + } else { + Command::new("login") + }; + unsafe { + command + .stdin(Stdio::from_raw_fd(slave_stdin as RawFd)) + .stdout(Stdio::from_raw_fd(slave_stdout as RawFd)) + .stderr(Stdio::from_raw_fd(slave_stderr as RawFd)) + .env("TERM", "xterm-256color") + .env("TTY", &pty); + } + + match command.spawn() { + Ok(mut process) => { + handle(&mut event_queue, tty_fd, master_fd, &mut process); + } + Err(err) => fail(&format!("getty: failed to execute login: {}", err), stderr), + } + } +} + +pub fn main() { + let mut stderr = io::stderr(); + + let args = clap_app!(getty => + (author: "Jeremy Soller") + (about: "Set terminal mode") + (@arg TTY: +required "") + (@arg NO_CLEAR: -J --("no-clear") "Do not clear the screen before forking") + (@arg CONTAIN: -C --("contain") "Run contain_login instead of login") + ) + .get_matches(); + + let clear = !args.is_present("NO_CLEAR"); + + let contain = args.is_present("CONTAIN"); + + let vt = args.value_of("TTY").unwrap(); + + let buf: String; + let vt_path = if vt.parse::().is_ok() { + buf = format!("/scheme/fbcon/{vt}"); + &*buf + } else { + vt + }; + + let mut tty = match redox::open( + &vt_path, + flag::O_CLOEXEC | flag::O_RDWR | flag::O_NONBLOCK, + 0, + ) { + Ok(fd) => unsafe { File::from_raw_fd(fd as RawFd) }, + Err(err) => fail( + &format!("getty: failed to open TTY {}: {}", vt_path, err), + &mut stderr, + ), + }; + + daemon(&mut tty, clear, contain, &mut stderr); +} diff --git a/src/bin/groupadd.rs b/src/bin/groupadd.rs new file mode 100644 index 0000000000..00e538cc1a --- /dev/null +++ b/src/bin/groupadd.rs @@ -0,0 +1,82 @@ +#[macro_use] +extern crate clap; + +use extra::option::OptionalExt; + +use std::process::exit; + +use redox_users::{All, AllGroups, Config, Error, GroupBuilder}; + +const _MAN_PAGE: &'static str = /* @MANSTART{groupadd} */ + r#" +NAME + groupadd - add a user group + +SYNOPSIS + groupadd [ -f | --force ] GROUP + groupadd [ -h | --help ] + +DESCRIPTION + The groupadd utility adds a new user group using values + passed on the command line and system defaults. + +OPTIONS + -f, --force + Simply forces the exit status of the program to 0 + even if the group already exists. A message is still + printed to stdout. + + -g, --gid GID + The group id to use. This value must not be used and must + be non-negative. The default is to pick the smallest available + group id (between values defined in redox_users). + + -h, --help + Display this help and exit. + +AUTHOR + Written by Wesley Hershberger. +"#; /* @MANEND */ + +fn main() { + let args = clap_app!(groupadd => + (author: "Wesley Hershberger") + (about: "Add groups based on the system's redox_users backend") + (@arg GROUP: +required "Add group GROUP") + (@arg FORCE: -f --force "Force the status of the program to be 0 even if the group exists") + (@arg GID: -g --gid +takes_value "Group id. Positive integer and must not be in use") + ) + .get_matches(); + + let mut sys_groups = AllGroups::new(Config::default().writeable(true)).unwrap_or_exit(1); + + let groupname = args.value_of("GROUP").unwrap(); + + let gid = match args.value_of("GID") { + Some(gid) => { + let id = gid.parse::().unwrap_or_exit(1); + if let Some(_group) = sys_groups.get_by_id(id) { + eprintln!("groupadd: group already exists"); + exit(1); + } + id + } + None => sys_groups.get_unique_id().unwrap_or_else(|| { + eprintln!("groupadd: no available gid"); + exit(1); + }), + }; + + let group = GroupBuilder::new(groupname).gid(gid); + match sys_groups.add_group(group) { + Ok(_) => (), + Err(Error::GroupAlreadyExists) if args.is_present("FORCE") => { + exit(0); + } + Err(err) => { + eprintln!("groupadd: {}", err); + exit(1); + } + } + sys_groups.save().unwrap_or_exit(1); +} diff --git a/src/bin/groupdel.rs b/src/bin/groupdel.rs new file mode 100644 index 0000000000..b369721c48 --- /dev/null +++ b/src/bin/groupdel.rs @@ -0,0 +1,47 @@ +#[macro_use] +extern crate clap; + +use extra::option::OptionalExt; +use redox_users::{All, AllGroups, Config}; + +const _MAN_PAGE: &'static str = /* @MANSTART{groupdel} */ + r#" +NAME + groupdel - modify system files to delete groups + +SYNOPSYS + groupdel [ options ] GROUP + groupdel [ -h | --help ] + +DESCRIPTION + groupdel removes groups from whatever backend is employed by + the system's redox_users. + + Note that you should not remove a primary user group before + removing the user. It is also generally wise not to remove + groups that still own files on the system. + +OPTIONS + -h, --help + Print this help page and exit. + +AUTHORS + Wesley Hershberger. +"#; /* @MANEND */ + +fn main() { + let matches = clap_app!(groupdel => + (author: "Wesley Hershberger") + (about: "Removes a group from the system using redox_users") + (@arg GROUP: +required "Removes group GROUP") + ) + .get_matches(); + + let group = matches.value_of("GROUP").unwrap(); + + let mut sys_groups = AllGroups::new(Config::default().writeable(true)).unwrap_or_exit(1); + + sys_groups.remove_by_name(group.to_string()); + + sys_groups.save().unwrap_or_exit(1); +} diff --git a/src/bin/groupmod.rs b/src/bin/groupmod.rs new file mode 100644 index 0000000000..ae80675882 --- /dev/null +++ b/src/bin/groupmod.rs @@ -0,0 +1,81 @@ +#[macro_use] +extern crate clap; + +use std::process::exit; + +use extra::option::OptionalExt; +use redox_users::{All, AllGroups, AllUsers, Config}; + +const _MAN_PAGE: &'static str = /* @MANSTART{groupmod} */ + r#" +NAME + groupmod - modify group information + +SYNOPSYS + groupmod [ options ] GROUP + groupmod [ -h | --help ] + +DESCRIPTION + groupmod modifies a user group GROUP in the system's + redox_users backend. + +OPTIONS + -h, --help + Print this help page and exit. + + -g, --gid GID + Change GROUP's group id. GID must be a non-negative + decimal integer. + + Files with GROUP's old gid will not be updated. + + User's who use the old gid as their primary gid will + be updated. + + -n, --name NAME + The name of the group will be set to NAME + +AUTHORS + Wesley Hershberger. +"#; /* @MANEND */ + +fn main() { + let args = clap_app!(groupmod => + (author: "Wesley Hershberger") + (about: "Modify users according to the system's redox_users backend") + (@arg GROUP: +required "Modify GROUP") + (@arg GID: -g --gid +takes_value "Change GROUP's group id. See man page for details") + (@arg NAME: -n --name +takes_value "Change GROUP's name") + ) + .get_matches(); + + let groupname = args.value_of("GROUP").unwrap(); + + let mut sys_groups = AllGroups::new(Config::default().writeable(true)).unwrap_or_exit(1); + { + let group = sys_groups.get_mut_by_name(groupname).unwrap_or_else(|| { + eprintln!("groupmod: group not found: {}", groupname); + exit(1); + }); + + if let Some(gid) = args.value_of("GID") { + let gid = gid.parse::().unwrap_or_exit(1); + // Update users + let mut sys_users = + AllUsers::authenticator(Config::default().writeable(true)).unwrap_or_exit(1); + for user in sys_users.iter_mut() { + if user.gid == group.gid { + user.gid = gid; + } + } + sys_users.save().unwrap_or_exit(1); + group.gid = gid; + } + + if let Some(name) = args.value_of("NAME") { + group.group = name.to_string(); + } + } + + sys_groups.save().unwrap_or_exit(1); +} diff --git a/src/bin/id.rs b/src/bin/id.rs new file mode 100644 index 0000000000..bda9cb1e58 --- /dev/null +++ b/src/bin/id.rs @@ -0,0 +1,163 @@ +#[macro_use] +extern crate clap; + +use std::env::args; +use std::process::exit; + +use extra::option::OptionalExt; +use redox_users::{All, AllGroups, AllUsers, Config, get_egid, get_euid, get_gid, get_uid}; + +const _MAN_PAGE: &'static str = /* @MANSTART{id} */ + r#" +NAME + id - display user identity + +SYNOPSIS + id + id -g [-nr] + id -u [-nr] + id [ -h | --help ] + +DESCRIPTION + The id utility displays the user and group names and numeric IDs, of + the calling process, to the standard output. + +OPTIONS + -G, --groups + Display the different group IDs (effective and real) as white-space + separated numbers, in no particular order. + + -g, --group + Display the effective group ID as a number. + + -n, --name + Display the name of the user or group ID for the -g and -u options + instead of the number. + + -u, --user + Display the effective user ID as a number. + + -a + Ignored for compatibility with other id implementations. + + -r, --real + Display the real ID for the -g and -u options instead of the effective ID. + + -h, --help + Display help and exit. + +AUTHOR + Written by Jose Narvaez. +"#; /* @MANEND */ + +pub fn main() { + let app = clap_app!(id => + (author: "Jose Narvaez") + (about: "Get user and group information about the current user") + (@arg IGNORE: -a "Ignored for compatibility with other impls of id") + (@arg GROUPS: -G --groups conflicts_with[selector modifier] "Display current user's real and effective group id's") + (@group selector => + (@arg GROUP: -g --group "Display current user's effective group id") + (@arg USER: -u --user "Display the effective userid") + ) + (@group modifier => + (@arg NAME: -n --name requires[selector] "Display names of groups/users instead of ids (use with -g or -u)") + (@arg REAL: -r --real requires[selector] "Display real id's instead of effective ids (use with -g and -u)") + ) + ); + + let args = match &*args().nth(0).unwrap_or(String::new()) { + "whoami" => app.get_matches_from(["id", "-un"].iter()), + _ => app.get_matches(), + }; + + // Display the different group IDs (effective and real) + // as white-space separated numbers, in no particular order. + if args.is_present("GROUPS") { + let egid = get_egid().unwrap_or_exit(1); + + let gid = get_gid().unwrap_or_exit(1); + + println!("{} {}", egid, gid); + exit(0); + } + + // Display effective/real process user ID UNIX user name + if args.is_present("USER") && args.is_present("NAME") { + // Did they pass -r? If so, we show the real + let uid = if args.is_present("REAL") { + get_uid() + } else { + get_euid() + } + .unwrap_or_exit(1); + + let users = AllUsers::basic(Config::default()).unwrap_or_exit(1); + let user = users.get_by_id(uid).unwrap_or_exit(1); + + println!("{}", user.user); + exit(0); + } + + // Display real user ID + if args.is_present("USER") && args.is_present("REAL") { + let uid = get_uid().unwrap_or_exit(1); + + println!("{}", uid); + exit(0); + } + + // Display effective user ID + if args.is_present("USER") { + let euid = get_euid().unwrap_or_exit(1); + + println!("{}", euid); + exit(0); + } + + // Display effective/real process group ID UNIX group name + if args.is_present("GROUP") && args.is_present("NAME") { + // Did they pass -r? If so we show the real one + let gid = if args.is_present("REAL") { + get_gid() + } else { + get_egid() + } + .unwrap_or_exit(1); + + let groups = AllGroups::new(Config::default()).unwrap_or_exit(1); + let group = groups.get_by_id(gid).unwrap_or_exit(1); + + println!("{}", group.group); + exit(0); + } + + // Display the real group ID + if args.is_present("GROUP") && args.is_present("REAL") { + let gid = get_gid().unwrap_or_exit(1); + + println!("{}", gid); + exit(0); + } + + // Display effective group ID + if args.is_present("GROUP") { + let egid = get_egid().unwrap_or_exit(1); + + println!("{}", egid); + exit(0); + } + + // We get everything we can and show + let euid = get_euid().unwrap_or_exit(1); + let egid = get_egid().unwrap_or_exit(1); + + let users = AllUsers::basic(Config::default()).unwrap_or_exit(1); + let groups = AllGroups::new(Config::default()).unwrap_or_exit(1); + + let user = users.get_by_id(euid).unwrap_or_exit(1); + let group = groups.get_by_id(egid).unwrap_or_exit(1); + + println!("uid={}({}) gid={}({})", euid, user.user, egid, group.group); + exit(0); +} diff --git a/src/bin/login.rs b/src/bin/login.rs new file mode 100644 index 0000000000..f7f337a5ed --- /dev/null +++ b/src/bin/login.rs @@ -0,0 +1,204 @@ +#[macro_use] +extern crate clap; + +use libredox::error::Result; +use std::fs::File; +use std::io::{self, Write}; +use std::str; + +use extra::option::OptionalExt; +use redox_users::{All, AllUsers, Config, User}; +use termion::input::TermRead; +use userutils::spawn_shell; + +const _MAN_PAGE: &'static str = /* @MANSTART{login} */ + r#" +NAME + login - log into the computer + +SYNOPSIS + login + +DESCRIPTION + The login utility logs users (and pseudo-users) into the computer system. + +OPTIONS + + -h --help + Display help info and exit. + +AUTHOR + Written by Jeremy Soller, Jose Narvaez. +"#; /* @MANEND */ + +const ISSUE_FILE: &'static str = "/etc/issue"; +const MOTD_FILE: &'static str = "/etc/motd"; + +// TODO: Move to redox_users once the definition solidifies. +const DEFAULT_SCHEMES: [&'static str; 26] = [ + // Kernel schemes + "debug", + "event", + "memory", + "pipe", + "serio", + "irq", + "time", + "sys", + // Base schemes + "rand", + "null", + "zero", + "log", + // Network schemes + "ip", + "icmp", + "tcp", + "udp", + // IPC schemes + "shm", + "chan", + "uds_stream", + "uds_dgram", + // File schemes + "file", + // Display schemes + "display.vesa", + "display*", + // Other schemes + "pty", + "sudo", + "audio", +]; +pub fn apply_login_schemes( + user: &User, + default_schemes: &[&str], +) -> Result { + let schemes = match load_config_schemes(user) { + Some(s) => s, + _ => default_schemes.iter().map(|s| s.to_string()).collect(), + }; + + let mut names: Vec = Vec::with_capacity(schemes.len()); + for scheme in schemes.iter() { + names.push(ioslice::IoSlice::new(scheme.as_bytes())); + } + + let ns_fd = libredox::call::mkns(&names)?; + let before_ns_fd = libredox::Fd::new(libredox::call::setns(ns_fd)?); + + Ok(before_ns_fd) +} + +fn load_config_schemes(user: &User) -> Option> { + use serde::{Deserialize, Serialize}; + use std::collections::BTreeMap; + use std::fs; + + const LOGIN_SCHEMES_FILE: &'static str = "/etc/login_schemes.toml"; + + #[derive(Debug, Clone, Serialize, Deserialize)] + struct UserSchemeConfig { + pub schemes: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + struct LoginConfig { + #[serde(rename = "user_schemes")] + pub user_schemes: BTreeMap, + } + + let config_str = fs::read_to_string(LOGIN_SCHEMES_FILE).ok()?; + let config: LoginConfig = toml::from_str(&config_str).ok()?; + + config + .user_schemes + .get(&user.user) + .map(|cfg| cfg.schemes.clone()) +} + +pub fn main() { + let mut stdout = io::stdout(); + let mut stderr = io::stderr(); + + let _args = clap_app!(login => + (author: "Jeremy Soller, Jose Narvaez") + (about: "Login as a user") + ) + .get_matches(); + + if let Ok(mut issue) = File::open(ISSUE_FILE) { + io::copy(&mut issue, &mut stdout).r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + } + + loop { + let user = liner::Context::new() + .read_line( + liner::Prompt::from("\x1B[1mRed Bear login:\x1B[0m "), + None, + &mut liner::BasicCompleter::new(Vec::::new()), + ) + .r#try(&mut stderr); + + if !user.is_empty() { + let stdin = io::stdin(); + let mut stdin = stdin.lock(); + let sys_users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); + + match sys_users.get_by_name(user) { + None => { + stdout.write(b"\nLogin incorrect\n").r#try(&mut stderr); + stdout.write(b"\n").r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + continue; + } + Some(user) => { + if user.is_passwd_blank() { + if let Ok(mut motd) = File::open(MOTD_FILE) { + io::copy(&mut motd, &mut stdout).r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + } + + let before_ns_fd = + apply_login_schemes(user, &DEFAULT_SCHEMES).unwrap_or_exit(1); + + let _ = syscall::fcntl( + before_ns_fd.raw(), + syscall::F_SETFD, + syscall::O_CLOEXEC, + ); + spawn_shell(user).unwrap_or_exit(1); + let _ = syscall::fcntl(before_ns_fd.raw(), syscall::F_SETFD, 0); + let _ = libredox::call::close( + libredox::call::setns(before_ns_fd.into_raw()).unwrap_or_exit(1), + ); + break; + } + + stdout + .write_all(b"\x1B[1mpassword:\x1B[0m ") + .r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + if let Some(password) = stdin.read_passwd(&mut stdout).r#try(&mut stderr) { + stdout.write(b"\n").r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + + if user.verify_passwd(&password) { + if let Ok(mut motd) = File::open(MOTD_FILE) { + io::copy(&mut motd, &mut stdout).r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + } + + spawn_shell(user).unwrap_or_exit(1); + break; + } + } + } + } + } else { + stdout.write(b"\n").r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + } + } +} diff --git a/src/bin/passwd.rs b/src/bin/passwd.rs new file mode 100644 index 0000000000..cacb4d09bf --- /dev/null +++ b/src/bin/passwd.rs @@ -0,0 +1,197 @@ +#[macro_use] +extern crate clap; + +use std::io; +use std::io::Write; +use std::process::exit; + +use extra::option::OptionalExt; +use libredox::flag::O_CLOEXEC; +use libredox::errno::EPERM; +use redox_users::{All, AllUsers, Config, get_uid}; +use termion::input::TermRead; + +const _MAN_PAGE: &'static str = /* @MANSTART{passwd} */ + r#" +NAME + passwd - modify a user's password + +SYNOPSIS + passwd [ LOGIN ] + passwd [ -h | --help ] + +DESCRIPTION + The passwd utility changes the user's local password. If the user is not + the super-user, passwd first prompts for the current password and will + not continue unless the correct password is entered. + +OPTIONS + + -h, --help + Display this help and exit. + + -l, --lock + Lock the password of the named account. This changes the stored password + hash so that it matches no encrypted value ("!") + + Users with locked passwords are not allowed to change their password. + +AUTHOR + Written by Jeremy Soller, Jose Narvaez. +"#; /* @MANEND */ + +fn main() { + let mut stdin = io::stdin().lock(); + let mut stdout = io::stdout().lock(); + let mut stderr = io::stderr(); + + let args = clap_app!(passwd => + (author: "Jeremy Soller, Jose Narvaez") + (about: "Set user passwords") + (@arg LOGIN: "Apply to login. Sets password for current user if not supplied") + (@arg LOCK: -l --lock "Lock the password for an account (no login)") + ) + .get_matches(); + + if args.is_present("LOCK") { + if get_uid().unwrap_or_exit(1) != 0 { + eprintln!("passwd: only root is allowed to lock accounts"); + exit(1); + } + + let mut users = + AllUsers::authenticator(Config::default().writeable(true)).unwrap_or_exit(1); + + let Some(login) = args.value_of("LOGIN") else { + eprintln!("passwd: no account specified to lock"); + exit(1); + }; + + let user = users.get_mut_by_name(login).unwrap_or_else(|| { + eprintln!("passwd: user does not exist: {}", login); + exit(1); + }); + + user.unset_passwd(); + users.save().unwrap_or_exit(1); + + return; + } + + let uid = get_uid().unwrap_or_exit(1); + + if uid == 0 { + let mut users = + AllUsers::authenticator(Config::default().writeable(true)).unwrap_or_exit(1); + + let user = find_user(&args, &mut users); + + let msg = format!("changing password for '{}' \n", user.user); + stdout.write_all(&msg.as_bytes()).r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + + let new_password = ask_new_password(stdin, stdout, stderr); + + user.set_passwd(&new_password).unwrap_or_exit(1); + users.save().unwrap_or_exit(1); + + return; + } + + let mut users = AllUsers::basic(Config::default()).unwrap_or_exit(1); + + let user = find_user(&args, &mut users); + + if user.uid != uid { + eprintln!( + "passwd: you do not have permission to set the password of '{}'", + user.user + ); + exit(1); + } + + let msg = format!("changing password for '{}' \n", user.user); + stdout.write_all(&msg.as_bytes()).r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + + drop(users); // Unlock /etc/passwd + + stdout.write_all(b"current password: ").r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + + let file = libredox::call::open("/scheme/sudo/passwd", O_CLOEXEC, 0).unwrap(); + + if let Some(password) = stdin.read_passwd(&mut stdout).r#try(&mut stderr) { + stdout.write(b"\n").r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + + match libredox::call::write(file, password.as_bytes()) { + Ok(_) => {} + Err(err) if err.errno() == EPERM => { + eprintln!("passwd: incorrect current password"); + exit(1); + } + Err(err) => panic!("{err}"), + } + } else { + eprintln!("passwd: incorrect current password"); + exit(1); + } + + let new_password = ask_new_password(stdin, stdout, stderr); + + match libredox::call::write(file, new_password.as_bytes()) { + Ok(_) => {} + Err(err) if err.errno() == EPERM => { + eprintln!("passwd: invalid new password"); + exit(1); + } + Err(err) => panic!("{err}"), + } +} + +fn find_user<'a, T: Default>( + args: &clap::ArgMatches<'_>, + users: &'a mut AllUsers, +) -> &'a mut redox_users::User { + let uid = get_uid().unwrap_or_exit(1); + match args.value_of("LOGIN") { + Some(login) => users.get_mut_by_name(login).unwrap_or_else(|| { + eprintln!("passwd: user does not exist: {}", login); + exit(1); + }), + None => users.get_mut_by_id(uid).unwrap_or_else(|| { + eprintln!("passwd: you do not exist"); + exit(1); + }), + } +} + +fn ask_new_password( + mut stdin: io::StdinLock<'_>, + mut stdout: io::StdoutLock<'_>, + mut stderr: io::Stderr, +) -> String { + stdout.write_all(b"new password: ").r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + let Some(new_password) = stdin.read_passwd(&mut stdout).r#try(&mut stderr) else { + eprintln!("passwd: no new password provided"); + exit(1); + }; + + stdout.write(b"\nconfirm password: ").r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + let Some(confirm_password) = stdin.read_passwd(&mut stdout).r#try(&mut stderr) else { + eprintln!("\npasswd: no confirm password provided"); + exit(1); + }; + + stdout.write(b"\n").r#try(&mut stderr); + stdout.flush().r#try(&mut stderr); + + if new_password != confirm_password { + eprintln!("passwd: new password does not match confirm password"); + exit(1); + } + new_password +} diff --git a/src/bin/su.rs b/src/bin/su.rs new file mode 100644 index 0000000000..49c3496706 --- /dev/null +++ b/src/bin/su.rs @@ -0,0 +1,84 @@ +#[macro_use] +extern crate clap; + +use std::io::{self, Write}; +use std::process::exit; +use std::str; + +use extra::option::OptionalExt; +use libredox::flag::O_CLOEXEC; +use redox_users::{All, AllUsers, Config, get_uid}; +use syscall::EPERM; +use termion::input::TermRead; +use userutils::spawn_shell; + +const _MAN_PAGE: &'static str = /* @MANSTART{su} */ + r#" +NAME + su - substitute user identity + +SYNOPSIS + su [ user ] + su [ -h | --help ] + +DESCRIPTION + The su utility requests appropriate user credentials via PAM and switches to + that user ID (the default user is the superuser). A shell is then executed. + +OPTIONS + + -h, --help + Display this help and exit. + +AUTHOR + Written by Jeremy Soller, Jose Narvaez. +"#; /* @MANEND */ + +pub fn main() { + let stdin = io::stdin(); + let mut stdin = stdin.lock(); + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + let mut stderr = io::stderr(); + + let args = clap_app!(su => + (author: "Jeremy Soller, Jose Narvaez") + (about: "substitue user identity") + (@arg LOGIN: "Login as LOGIN. Default is \'root\'") + ) + .get_matches(); + + let target_user = args.value_of("LOGIN").unwrap_or("root"); + + let uid = get_uid().unwrap_or_exit(1); + + let users = AllUsers::basic(Config::default()).unwrap_or_exit(1); + let user = users.get_by_name(&target_user).unwrap_or_exit(1); + + // If the user executing su is root, then they can do anything without a password. + // Same if the user we're being asked to login as doesn't have a password. + if uid == 0 { + writeln!(stdout).unwrap_or_exit(1); + exit(spawn_shell(user).unwrap_or_exit(1)); + } else { + let file = libredox::call::open("/scheme/sudo/su", O_CLOEXEC, 0).unwrap(); + + write!(stdout, "password: ").unwrap_or_exit(1); + stdout.flush().unwrap_or_exit(1); + + // Read the password, reading an empty string if CTRL-d is specified + let password = stdin + .read_passwd(&mut stdout) + .r#try(&mut stderr) + .unwrap_or(String::new()); + + match libredox::call::write(file, password.as_bytes()) { + Ok(_) => exit(spawn_shell(user).unwrap_or_exit(1)), + Err(err) if err.errno() == EPERM => { + writeln!(stderr, "su: authentication failed").unwrap_or_exit(1); + exit(1); + } + Err(err) => panic!("{err}"), + } + } +} diff --git a/src/bin/sudo.rs b/src/bin/sudo.rs new file mode 100644 index 0000000000..9959b14117 --- /dev/null +++ b/src/bin/sudo.rs @@ -0,0 +1,401 @@ +use std::collections::HashMap; +use std::env; +use std::io::{self, Write}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; +use std::os::unix::process::CommandExt; +use std::process::{Command, exit}; + +use extra::option::OptionalExt; +use libredox::flag::O_CLOEXEC; +use redox_rt::protocol::ProcCall; +use redox_rt::sys::proc_call; +use redox_scheme::scheme::{SchemeSync, register_sync_scheme}; +use redox_scheme::{ + CallerCtx, OpenResult, RequestKind, Response, SendFdRequest, SignalBehavior, Socket, +}; +use redox_users::{All, AllGroups, AllUsers, Config, get_uid}; +use syscall::error::*; +use syscall::flag::*; +use syscall::schemev2::NewFdFlags; +use termion::input::TermRead; + +const MAX_ATTEMPTS: u16 = 3; +const _MAN_PAGE: &'static str = /* @MANSTART{sudo} */ + r#" +NAME + sudo - execute a command as another user + +SYNOPSIS + sudo command + sudo [ -h | --help ] + +DESCRIPTION + The sudo utility allows a permitted user to execute a command as the + superuser or another user, as specified by the security policy. + +EXIT STATUS + Upon successful execution of a command, the exit status from sudo will + be the exit status of the program that was executed. In case of error + the exit status will be >0. + +AUTHOR + Written by Jeremy Soller, Jose Narvaez, bjorn3. +"#; /* @MANEND */ + +fn main() { + if env::args().nth(1).as_deref() == Some("--daemon") { + daemon_main(); + } + + let mut args = env::args().skip(1); + let cmd = args.next().unwrap_or_else(|| { + eprintln!("sudo: no command provided"); + exit(1); + }); + + let users = AllUsers::basic(Config::default()).unwrap_or_exit(1); + let uid = get_uid().unwrap_or_exit(1); + let user = users.get_by_id(uid).unwrap_or_exit(1); + + if uid == 0 { + // We are root already. No need to elevate privileges + run_command_as_root(&cmd, &args.collect()); + } + + let file = libredox::call::open("/scheme/sudo", O_CLOEXEC, 0).unwrap(); + + let mut attempts = 0; + + loop { + print!("[sudo] password for {}: ", user.user); + let _ = io::stdout().flush(); + + match io::stdin().read_passwd(&mut io::stdout()).unwrap() { + Some(password) => { + println!(); + + match libredox::call::write(file, password.as_bytes()) { + Ok(_) => break, + Err(err) if err.errno() == EPERM => { + attempts += 1; + eprintln!( + "sudo: incorrect password or not in sudo group ({}/{})", + attempts, MAX_ATTEMPTS, + ); + if attempts >= MAX_ATTEMPTS { + exit(1); + } + } + Err(err) => panic!("{err}"), + } + } + None => { + println!(); + exit(1); + } + } + } + + // FIXME move to libredox + unsafe extern "C" { + safe fn redox_cur_procfd_v0() -> usize; + } + + // Elevate privileges of our own process with help from the sudo daemon + syscall::sendfd( + file, + syscall::dup(redox_cur_procfd_v0(), &[]).unwrap(), + 0, + 0, + ) + .unwrap(); + + // FIXME perhaps keep the original namespace available in a subdirectory of the namespace we switch to? + let ns = syscall::openat(file, "ns", 0, syscall::O_CLOEXEC).unwrap(); + libredox::call::setns(ns).unwrap(); + + run_command_as_root(&cmd, &args.collect()); +} + +enum Policy { + Deny, + Authenticate, +} + +fn policy_for_user(uid: u32) -> Policy { + let users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); + let groups = AllGroups::new(Config::default()).unwrap_or_exit(1); + + let user = users.get_by_id(uid as usize).unwrap_or_exit(1); + + let sudo_group = groups.get_by_name("sudo").unwrap_or_exit(1); + if !sudo_group.users.iter().any(|name| name == &user.user) { + return Policy::Deny; + } + + Policy::Authenticate +} + +fn run_command_as_root(cmd: &str, args: &Vec) -> ! { + let mut command = Command::new(&cmd); + for arg in args { + command.arg(&arg); + } + + command.uid(0); + command.gid(0); + command.env("USER", "root"); + command.env("UID", "0"); + command.env("GROUPS", "0"); + + let err = command.exec(); + + eprintln!("sudo: failed to execute {}: {}", cmd, err); + exit(1); +} + +struct Scheme { + next_fd: usize, + handles: HashMap, +} +enum Handle { + AwaitingPassword { uid: u32 }, + AwaitingRootPassword, + AwaitingContextFd, + AwaitingNamespaceFetch { ns: libredox::Fd }, + + AwaitingPasswordForPasswd { uid: u32 }, + AwaitingNewPassword { uid: u32 }, + + Placeholder, + + SchemeRoot, +} + +impl SchemeSync for Scheme { + fn scheme_root(&mut self) -> Result { + let fd = self.next_fd; + self.next_fd = self.next_fd.checked_add(1).ok_or(Error::new(EMFILE))?; + self.handles.insert(fd, Handle::SchemeRoot); + Ok(fd) + } + fn openat( + &mut self, + dirfd: usize, + path: &str, + _flags: usize, + _fcntl_flags: u32, + ctx: &CallerCtx, + ) -> Result { + let handle = match self.handles.get_mut(&dirfd).ok_or(Error::new(EBADF))? { + Handle::SchemeRoot => match path { + "" => Handle::AwaitingPassword { uid: ctx.uid }, + "su" => Handle::AwaitingRootPassword, + "passwd" => Handle::AwaitingPasswordForPasswd { uid: ctx.uid }, + _ => return Err(Error::new(ENOENT)), + }, + Handle::AwaitingNamespaceFetch { .. } => { + if path != "ns" { + return Err(Error::new(ENOENT)); + } + let ns = match self.handles.insert(dirfd, Handle::Placeholder).unwrap() { + Handle::AwaitingNamespaceFetch { ns } => ns, + _ => unreachable!(), + }; + return Ok(OpenResult::OtherScheme { fd: ns.into_raw() }); + } + _ => return Err(Error::new(EINVAL)), + }; + + let fd = self.next_fd; + self.next_fd = self.next_fd.checked_add(1).ok_or(Error::new(EMFILE))?; + self.handles.insert(fd, handle); + + Ok(OpenResult::ThisScheme { + number: fd, + flags: NewFdFlags::empty(), + }) + } + + fn write( + &mut self, + id: usize, + buf: &[u8], + _off: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?; + + let validate_utf8 = |buf| std::str::from_utf8(buf).map_err(|_| Error::new(EINVAL)); + + match std::mem::replace(handle, Handle::Placeholder) { + Handle::AwaitingPassword { uid } => { + let users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); + let user = users.get_by_id(uid as usize).unwrap_or_exit(1); + + match policy_for_user(uid) { + Policy::Deny => { + *handle = Handle::AwaitingPassword { uid }; + return Err(Error::new(EPERM)); + } + Policy::Authenticate => { + let password = validate_utf8(buf)?; + if user.verify_passwd(&password) { + *handle = Handle::AwaitingContextFd + } else { + *handle = Handle::AwaitingPassword { uid }; + return Err(Error::new(EPERM)); + } + } + } + } + Handle::AwaitingRootPassword => { + let users = AllUsers::authenticator(Config::default()).unwrap_or_exit(1); + let user = users.get_by_id(0).unwrap_or_exit(1); + + let password = validate_utf8(buf)?; + if user.verify_passwd(&password) { + *handle = Handle::AwaitingContextFd + } else { + *handle = Handle::AwaitingRootPassword; + return Err(Error::new(EPERM)); + } + } + Handle::AwaitingContextFd => { + *handle = Handle::AwaitingContextFd; + return Err(Error::new(EINVAL)); + } + + Handle::AwaitingPasswordForPasswd { uid } => { + let users = + AllUsers::authenticator(Config::default()).map_err(|_| Error::new(ENOLCK))?; + let user = users.get_by_id(uid as usize).ok_or(Error::new(EEXIST))?; + + let password = validate_utf8(buf)?; + if user.verify_passwd(&password) { + *handle = Handle::AwaitingNewPassword { uid } + } else { + *handle = Handle::AwaitingPasswordForPasswd { uid }; + return Err(Error::new(EPERM)); + } + } + Handle::AwaitingNewPassword { uid } => { + let mut users = AllUsers::authenticator(Config::default().writeable(true)) + .map_err(|_| Error::new(ENOLCK))?; + let user = users + .get_mut_by_id(uid as usize) + .ok_or(Error::new(EEXIST))?; + + let new_password = validate_utf8(buf)?; + if user.set_passwd(&new_password).is_ok() { + users.save().map_err(|_| Error::new(ENOLCK))?; + *handle = Handle::Placeholder + } else { + *handle = Handle::AwaitingNewPassword { uid }; + return Err(Error::new(EPERM)); + } + } + + Handle::AwaitingNamespaceFetch { .. } => { + eprintln!("sudo: found namespace fetch handle with ID {id}"); + return Err(Error::new(EBADFD)); + } + + Handle::Placeholder => { + eprintln!("sudo: found placeholder handle with ID {id}"); + return Err(Error::new(EBADFD)); + } + + Handle::SchemeRoot => { + eprintln!("sudo: found Scheme root handle with ID {id}"); + return Err(Error::new(EBADFD)); + } + } + Ok(buf.len()) + } +} +impl Scheme { + fn on_close(&mut self, id: usize) { + self.handles.remove(&id); + } + + fn on_sendfd(&mut self, socket: &Socket, req: &SendFdRequest) -> Result { + let handle = self.handles.get_mut(&req.id()).ok_or(Error::new(EBADF))?; + match std::mem::replace(handle, Handle::Placeholder) { + Handle::AwaitingContextFd => { + let mut proc_fd = usize::MAX; + req.obtain_fd( + socket, + FobtainFdFlags::empty(), + std::slice::from_mut(&mut proc_fd), + )?; + let proc_fd = unsafe { OwnedFd::from_raw_fd(proc_fd as RawFd) }; + + let [ruid, euid, suid] = [0, 0, 0]; + let [rgid, egid, sgid] = [0, 0, 0]; + let mut payload = [0; size_of::() * 6]; + plain::slice_from_mut_bytes(&mut payload) + .unwrap() + .copy_from_slice(&[ruid, euid, suid, rgid, egid, sgid]); + + if let Err(err) = proc_call( + proc_fd.as_raw_fd() as usize, + &mut payload, + CallFlags::empty(), + &[ProcCall::SetResugid as u64], + ) { + eprintln!("failed to setresugid: {err}"); + } + + *handle = Handle::AwaitingNamespaceFetch { + ns: libredox::Fd::new( + syscall::dup(libredox::call::getns().unwrap(), b"").unwrap(), + ), + }; + } + old => { + *handle = old; + return Err(Error::new(EBADF)); + } + } + Ok(0) + } +} + +fn daemon_main() -> ! { + // TODO: Linux kernel audit-like logging? + let socket = Socket::create().expect("failed to open scheme socket"); + + let mut scheme = Scheme { + next_fd: 1, + handles: HashMap::new(), + }; + + register_sync_scheme(&socket, "sudo", &mut scheme) + .expect("failed to register sudo scheme to namespace"); + + loop { + let Some(req) = socket + .next_request(SignalBehavior::Restart) + .expect("failed to get request") + else { + break; + }; + + let response = match req.kind() { + RequestKind::Call(call) => call.handle_sync(&mut scheme), + RequestKind::SendFd(req) => Response::new(scheme.on_sendfd(&socket, &req), req), + RequestKind::OnClose { id } => { + scheme.on_close(id); + continue; + } + _ => continue, + }; + + socket + .write_response(response, SignalBehavior::Restart) + .expect("sudo: scheme write failed"); + } + std::process::exit(0) +} diff --git a/src/bin/useradd.rs b/src/bin/useradd.rs new file mode 100644 index 0000000000..2cb267f528 --- /dev/null +++ b/src/bin/useradd.rs @@ -0,0 +1,205 @@ +#[macro_use] +extern crate clap; + +use std::process::exit; + +use extra::option::OptionalExt; +use redox_users::{All, AllGroups, AllUsers, Config, GroupBuilder, UserBuilder}; +use userutils::create_user_dir; + +const _MAN_PAGE: &'static str = /* @MANSTART{useradd} */ + r#" +NAME + useradd - add a new user + +SYNOPSYS + useradd [ options ] LOGIN + useradd [ -h | --help ] + +DESCRIPTION + The useradd utility creates a new user based on + system defaults and values passed on the command line. + + Useradd creates a new group for the user by default and + can also be instructed to create the user's home directory. + + Note that useradd creates a new user with the password + unset (no login). This is better documented with the + redox_users crate. + +OPTIONS + -h, --help + Display this help and exit. + + -c, --comment + Any text string, usually used as the user's full name. + Historically known as the GECOS field + + -d, --home-dir HOME_DIR + The new user will be created with HOME_DIR as their home + directory. The default value is LOGIN prepended with "/home/". + This flag DOES NOT create the home directory. See --create-home + + -g, --gid GID + The group id to use when creating the default login group. This value + must not be in use and must be non-negative. The default is to pick the + smallest available group id between values defined in redox_users. + + -m, --create-home + Creates the user's home directory if it does not already exist. + + This option is not enabled by default. This option must be specified + for a home directory to be created. If not set, the user's home dir is + set to "/" + + -N, --no-user-group + Do not attempt to create the user's user group. Instead, the groupid + is set to 99 ("nobody"). -N and -g are mutually exclusive. + + -s, --shell SHELL + The path to the user's default login shell. If not specified, the + default shell is set as "/bin/ion" + + -u, --uid UID + The user id to use. This value must not be in use and must be + non-negative. The default is to pick the smallest available + user id between the defaults defined in redox_users + +AUTHORS + Written by Wesley Hershberger. +"#; /* @MANEND */ +const DEFAULT_SHELL: &'static str = "/bin/ion"; +const DEFAULT_HOME: &'static str = "/home"; +const DEFAULT_NO_GROUP: &'static str = "nobody"; + +fn main() { + let args = clap_app!(useradd => + (author: "Wesley Hershberger") + (about: "Add users based on the system's redox_users backend") + (@arg LOGIN: + +required + "Add user LOGIN") + (@arg COMMENT: + -c --comment + +takes_value + "Set user description (GECOS field)") + (@arg HOME_DIR: + -d --("home-dir") + +takes_value + "Set LOGIN's home dir to HOME_DIR (does not create directory)") + (@arg CREATE_HOME: + -m --("create-home") + "Create the user's home directory") + (@arg SHELL: + -s --shell + +takes_value + "Set user's default login shell") + (@arg GID: + -g --gid + +takes_value + "Set LOGIN's primary group id. Positive integer and must not be in use.") + (@arg NO_USER_GROUP: + -N --("no-user-group") + conflicts_with[GID] + "Do not create primary user group (set gid to 99, \"nobody\")") + (@arg UID: + -u --uid + +takes_value + "Set LOGIN's user id. Positive ineger and must not be in use.") + ) + .get_matches(); + + // unwrap is safe because of "+required". clap-rs is cool... + let login = args.value_of("LOGIN").unwrap(); + + let mut sys_users = + AllUsers::authenticator(Config::default().writeable(true)).unwrap_or_exit(1); + let mut sys_groups = AllGroups::new(Config::default().writeable(true)).unwrap_or_exit(1); + + let uid = match args.value_of("UID") { + Some(uid) => { + let id = uid.parse::().unwrap_or_exit(1); + if let Some(_user) = sys_users.get_by_id(id) { + eprintln!("useradd: userid already in use: {}", id); + exit(1); + } + id + } + None => sys_users.get_unique_id().unwrap_or_else(|| { + eprintln!("useradd: no available uid"); + exit(1); + }), + }; + + let gid = if args.is_present("NO_USER_GROUP") { + let nobody = sys_groups + .get_mut_by_name(DEFAULT_NO_GROUP) + .unwrap_or_else(|| { + eprintln!("useradd: group not found: {}", DEFAULT_NO_GROUP); + exit(1) + }); + nobody.users.push(login.to_string()); + 99 + } else { + let id = match args.value_of("GID") { + Some(id) => { + let id = id.parse::().unwrap_or_exit(1); + if let Some(_group) = sys_groups.get_by_id(id) { + eprintln!("useradd: group already exists with gid: {}", id); + exit(1); + } + id + } + None => sys_groups.get_unique_id().unwrap_or_else(|| { + eprintln!("useradd: no available gid"); + exit(1); + }), + }; + sys_groups + .add_group(GroupBuilder::new(login).gid(id).user(login)) + .unwrap_or_else(|err| { + eprintln!("useradd: {}: {}", err, login); + exit(1); + }); + id + }; + + let gecos = args.value_of("COMMENT").unwrap_or(login); + + //Ugly way to satisfy the borrow checker... + let mut sys_homes = String::from(DEFAULT_HOME); + let userhome = args.value_of("HOME_DIR").unwrap_or_else(|| { + if args.is_present("CREATE_HOME") { + sys_homes.push_str("/"); + sys_homes.push_str(&login); + sys_homes.as_str() + } else { + "/" + } + }); + + let shell = args.value_of("SHELL").unwrap_or(DEFAULT_SHELL); + + let user = UserBuilder::new(login) + .uid(uid) + .gid(gid) + .name(gecos) + .home(userhome) + .shell(shell); + sys_users.add_user(user).unwrap_or_else(|err| { + eprintln!("useradd: {}: {}", err, login); + exit(1); + }); + + // Make sure to try and create the user/groups before we create + // their home, that way we get a permissions error that makes + // more sense + sys_groups.save().unwrap_or_exit(1); + sys_users.save().unwrap_or_exit(1); + + if args.is_present("CREATE_HOME") { + //Shouldn't ever error... + let user = sys_users.get_by_id(uid).unwrap_or_exit(1); + create_user_dir(user, userhome).unwrap_or_exit(1); + } +} diff --git a/src/bin/userdel.rs b/src/bin/userdel.rs new file mode 100644 index 0000000000..cd0275ce56 --- /dev/null +++ b/src/bin/userdel.rs @@ -0,0 +1,69 @@ +#[macro_use] +extern crate clap; + +use std::fs::remove_dir; +use std::process::exit; + +use extra::option::OptionalExt; +use redox_users::{All, AllGroups, AllUsers, Config}; +use userutils::AllGroupsExt; + +const _MAN_PAGE: &'static str = /* @MANSTART{userdel} */ + r#" +NAME + userdel - modify system files to delete users + +SYNOPSYS + userdel [ options ] LOGIN + userdel [ -h | --help ] + +DESCRIPTION + userdel removes users from whatever backend is employed by + the system's redox_users. The utility removes the user from + all groups of which they are a member. + + It can also be used to manage removal of home directories. + +OPTIONS + -h, --help + Print this help page and exit. + + -r, --remove + The user's home directory and all files inside will be + removed. + +AUTHORS + Wesley Hershberger. +"#; /* @MANEND */ + +fn main() { + let args = clap_app!(userdel => + (author: "Wesley Hershberger") + (about: "Removes system users using redox_users") + (@arg LOGIN: +required "Remove user LOGIN") + (@arg REMOVE: -r --remove "Remove the user's home and all files and directories inside") + ) + .get_matches(); + + let login = args.value_of("LOGIN").unwrap(); + + let mut sys_users = + AllUsers::authenticator(Config::default().writeable(true)).unwrap_or_exit(1); + let mut sys_groups = AllGroups::new(Config::default().writeable(true)).unwrap_or_exit(1); + { + sys_groups.remove_user_from_all_groups(login); + + if args.is_present("REMOVE") { + let user = sys_users.get_by_name(login).unwrap_or_else(|| { + eprintln!("userdel: user does not exist: {}", login); + exit(1); + }); + remove_dir(&user.home).unwrap_or_exit(1); + } + } + + sys_users.remove_by_name(login.to_string()); + + sys_groups.save().unwrap_or_exit(1); + sys_users.save().unwrap_or_exit(1); +} diff --git a/src/bin/usermod.rs b/src/bin/usermod.rs new file mode 100644 index 0000000000..2a02a7b22d --- /dev/null +++ b/src/bin/usermod.rs @@ -0,0 +1,196 @@ +#[macro_use] +extern crate clap; + +use std::fs::{remove_dir, rename}; +use std::process::exit; + +use extra::option::OptionalExt; +use redox_users::{All, AllGroups, AllUsers, Config}; +use userutils::{AllGroupsExt, create_user_dir}; + +const _MAN_PAGE: &'static str = /* @MANSTART{usermod} */ + r#" +NAME + usermod - modify user information + +SYNOPSYS + usermod [ options ] LOGIN + usermod [ -h | --help ] + +DESCRIPTION + The usermod utility can be used to modify user information. + + This utility uses the redox_users API, so the backend is whatever + backend in use on the system for that API at the time. + + See passwd for setting user passwords. + +OPTIONS + -h, --help + Display this help and exit. + + -c, --comment COMMENT + The comment field (or GECOS, historically) for the user. This + is typically the full name of the user, although sometimes it + includes an e-mail. + + -d, --home-dir HOME_DIR + Sets the home directory to HOME_DIR and creates the directory. + See -m for move + + -m, --move-home + Moves the the user's old home directory into the home directory + specified by --home-dir. Has no effect if passed without --home-dir + + -G, --append-groups GROUP[,GROUP, ...] + Add this user to GROUP groups. This does not remove the user from + any group of which they are already a member. + + -S, --set-groups GROUP[,GROUP, ...] + Remove the user from all groups of which they are a part and add + them to GROUP groups. + + -g, --gid GID + Set the user's primary group id. If the group does not exist, + a warning is issued and no changes are applied. + + -l, --login NEW_LOGIN + Set the new login name for the user. Must not be in use. + + -s, --shell SHELL + Set the user's login shell as SHELL. This must be a full path. + + -u, --uid UID + Set the user's user id. If another user's userid is the same as + UID, a warning is issued and no changes are applied. Note that + changing the value of the user's userid may have unexpected consequences. + +AUTHORS + Written by Wesley Hershberger. +"#; /* @MANEND */ + +fn main() { + let args = clap_app!(usermod => + (author: "Wesley Hershberger") + (about: "Modify users according to the system's redox_users backend") + (@arg LOGIN: + +required + "Apply modifications to LOGIN") + (@arg COMMENT: + -c --comment + +takes_value + "Set LOGIN's description (GECOS field)") + (@arg HOME_DIR: + -d --("home-dir") + +takes_value + "Create and set LOGIN's home directory") + (@arg MOVE_HOME: + -m --("move-home") + requires[HOME_DIR] + "Move LOGIN's old home to HOME_DIR (see --home-dir) instead of creating it. Requires -d") + (@arg APPEND_GROUPS: + -G --("append-groups") + +takes_value conflicts_with[SET_GROUPS] + "Add user to groups specified (comma separated list, see man page)") + (@arg SET_GROUPS: + -S --("set-groups") + +takes_value conflicts_with[APPEND_GROUPS] + "Set LOGIN's groups as specified (truncates existing, see man page)") + (@arg GID: + -g --gid + +takes_value + "Set LOGIN's primary group id. Group must exist") + (@arg NEW_LOGIN: + -l --login + +takes_value + "Set LOGIN's name to NEW_LOGIN") + (@arg SHELL: + -s --shell + +takes_value + "Set LOGIN's default login shell") + (@arg UID: + -u --uid + +takes_value + "Set LOGIN's user id. See man page for details") + ).get_matches(); + + let login = args.value_of("LOGIN").unwrap(); + + //TODO: Does not always need shadowfile access + let mut sys_users = + AllUsers::authenticator(Config::default().writeable(true)).unwrap_or_exit(1); + let mut sys_groups; + + if let Some(new_groups) = args.value_of("SET_GROUPS") { + sys_groups = AllGroups::new(Config::default().writeable(true)).unwrap_or_exit(1); + sys_groups.remove_user_from_all_groups(login); + sys_groups + .add_user_to_groups(login, new_groups.split(',').collect()) + .unwrap_or_exit(1); + sys_groups.save().unwrap_or_exit(1); + } + + if let Some(new_groups) = args.value_of("APPEND_GROUPS") { + sys_groups = AllGroups::new(Config::default().writeable(true)).unwrap_or_exit(1); + sys_groups + .add_user_to_groups(login, new_groups.split(',').collect()) + .unwrap_or_exit(1); + sys_groups.save().unwrap_or_exit(1); + } + + let uid = args.value_of("UID").map(|uid| { + let uid = uid.parse::().unwrap_or_exit(1); + if let Some(_user) = sys_users.get_by_id(uid) { + eprintln!("usermod: userid already in use: {}", uid); + exit(1); + } + uid + }); + + { + let user = sys_users.get_mut_by_name(&login).unwrap_or_else(|| { + eprintln!("usermod: user \"{}\" not found", login); + exit(1); + }); + + if let Some(gecos) = args.value_of("COMMENT") { + user.name = gecos.to_string(); + } + + if let Some(new_login) = args.value_of("NEW_LOGIN") { + user.user = new_login.to_string(); + } + + if let Some(shell) = args.value_of("SHELL") { + user.shell = shell.to_string(); + } + + if let Some(home) = args.value_of("HOME_DIR") { + if args.is_present("MOVE_HOME") { + rename(&user.home, &home).unwrap_or_exit(1); + } else { + create_user_dir(user, &home).unwrap_or_exit(1); + remove_dir(&user.home).unwrap_or_exit(1); + } + user.home = home.to_string(); + } + + if let Some(uid) = uid { + user.uid = uid; + } + + if let Some(gid) = args.value_of("GID") { + sys_groups = AllGroups::new(Config::default()).unwrap_or_exit(1); + let gid = gid.parse::().unwrap_or_exit(1); + + if let Some(_group) = sys_groups.get_by_id(gid) { + user.gid = gid; + } else { + eprintln!("usermod: no group found for id: {}", gid); + exit(1); + } + } + } + + sys_users.save().unwrap_or_exit(1); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000..2789547063 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,92 @@ +//! Redox OS user and group utilities. +//! +//! The `userutils` crate contains the utilities for dealing with users and groups in Redox OS. +//! They are heavily influenced by UNIX and are, when needed, tailored to specific Redox use cases. +//! +//! These implementations strive to be as simple as possible drawing particular +//! inspiration by BSD systems. They are indeed small, by choice. +//! +//! The included utilities are: +//! +//! - `getty`: Used by `init(8)` to open and initialize the TTY line, read a login name and invoke `login(1)`. +//! - `id`: Displays user identity. +//! - `login`: Allows users to into the system. +//! - `passwd`: Allows users to modify their passwords. +//! - `su`: Allows users to substitute identity. +//! - `sudo`: Enables users to execute a command as another user. +//! - `whoami`: Display effective user ID. + +use std::io::Result as IoResult; + +use libredox::call::{fchown, open}; +use libredox::error::Result as SysResult; +use libredox::flag::{O_CLOEXEC, O_CREAT, O_DIRECTORY}; +use redox_users::{All, AllGroups, Error, Result, User, auth}; + +const DEFAULT_MODE: u16 = 0o700; + +// Not the prettiest thing in the world, but some functionality here makes +// some of the utils much less gross +pub trait AllGroupsExt { + fn add_user_to_groups(&mut self, login: &str, groups: Vec<&str>) -> Result<()>; + fn remove_user_from_all_groups(&mut self, login: &str); +} + +impl AllGroupsExt for AllGroups { + // new_groups is a comma separated list of groupnames + fn add_user_to_groups(&mut self, login: &str, new_groups: Vec<&str>) -> Result<()> { + for groupname in new_groups { + let group = match self.get_mut_by_name(groupname) { + Some(group) => group, + None => return Err(Error::UserNotFound), + }; + group.users.push(login.to_string()); + } + Ok(()) + } + + /// Remove a user from all groups of which they are a member + fn remove_user_from_all_groups(&mut self, login: &str) { + for group in self.iter_mut() { + let op_pos = group.users.iter().position(|username| username == login); + if let Some(indx) = op_pos { + group.users.remove(indx); + } + } + } +} + +/// Spawns a shell for the given `User`. +/// +/// This function wraps the shell_cmd function of the User struct +/// from redox_users and manages the child process. It is a blocking +/// operation. +/// +/// # Examples +/// +/// ``` +/// use redox_users::AllUsers; +/// +/// let sys_users = AllUsers::new().unwrap(); +/// let user = sys_users.get_by_name("goyox86"); +/// spawn_shell(user).unwrap(); +/// ``` +pub fn spawn_shell(user: &User) -> IoResult { + let mut command = user.shell_cmd(); + + let mut child = command.spawn()?; + match child.wait()?.code() { + Some(code) => Ok(code), + None => Ok(1), + } +} + +/// Creates a directory with 700 user:user permissions +pub fn create_user_dir(user: &User, dir: T) -> SysResult<()> +where + T: AsRef + std::convert::AsRef<[u8]>, +{ + let fd = open(dir, O_CREAT | O_DIRECTORY | O_CLOEXEC, DEFAULT_MODE)?; + fchown(fd, user.uid as u32, user.gid as u32)?; + Ok(()) +}