├── .dockerignore ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── audit.yml │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── BUG-BOUNTY.md ├── Cargo.toml ├── Dockerfile ├── LICENSE-APACHE ├── README.md ├── contrib ├── DEBIAN │ ├── conffiles │ ├── control │ ├── md5sums │ └── postinst ├── make_deb_pkg.sh └── sudo_pair.spec ├── demo.gif ├── examples └── raw_plugin_api │ ├── Cargo.toml │ ├── LICENSE-APACHE │ └── src │ └── lib.rs ├── sample ├── Makefile ├── bin │ └── sudo_approve └── etc │ ├── sudo.conf │ ├── sudo_pair.prompt.pair │ ├── sudo_pair.prompt.user │ └── sudoers.d │ └── sudo_pair ├── sudo_pair ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── README.md └── src │ ├── errors.rs │ ├── lib.rs │ ├── socket.rs │ └── template.rs ├── sudo_plugin-sys ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── README.md ├── build.rs ├── include │ └── sudo_plugin.h └── src │ ├── bindings │ ├── sudo_plugin.aarch64.rs │ ├── sudo_plugin.x86-64.rs │ └── sudo_plugin.x86.rs │ └── lib.rs └── sudo_plugin ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE └── src ├── core.rs ├── errors.rs ├── lib.rs ├── macros.rs ├── options.rs ├── options ├── command_info.rs ├── option_map.rs ├── settings.rs ├── traits.rs └── user_info.rs ├── output.rs ├── output ├── print_facility.rs └── tty.rs ├── plugin.rs ├── plugin ├── io_env.rs ├── io_plugin.rs └── io_state.rs ├── prelude.rs └── version.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | 3 | /Cargo.lock 4 | /target 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @stephen @mcpherrinm @klieth 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security Audit 2 | 3 | on: 4 | 5 | push: 6 | paths: 7 | - '**/Cargo.toml' 8 | - '**/Cargo.lock' 9 | 10 | schedule: 11 | - cron: '3 17 * * *' 12 | 13 | jobs: 14 | 15 | audit: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: git checkout 20 | uses: actions/checkout@v1 21 | 22 | - name: cargo audit 23 | uses: actions-rs/audit-check@v1 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ 'master' ] 6 | tags: [ 'v*' ] 7 | pull_request: 8 | branches: [ 'master' ] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | RUSTFLAGS: -A unknown-lints -D warnings 13 | 14 | jobs: 15 | 16 | ci: 17 | name: Build and Test 18 | 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | matrix: 23 | rust: 24 | - stable 25 | - beta 26 | - nightly 27 | - 1.52 # MSRV 28 | 29 | steps: 30 | - name: git checkout 31 | uses: actions/checkout@v2 32 | 33 | - name: cache crates 34 | uses: actions/cache@v2 35 | with: 36 | path: | 37 | ~/.cargo/registry 38 | ~/.cargo/git 39 | target 40 | key: ${{ runner.os }}-rust-${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.toml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-rust-${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.toml') }} 43 | ${{ runner.os }}-rust-${{ matrix.rust }}-cargo 44 | ${{ runner.os }}-rust 45 | 46 | - name: rustup install 47 | uses: actions-rs/toolchain@v1 48 | with: 49 | toolchain: ${{ matrix.rust }} 50 | profile: minimal 51 | override: true 52 | 53 | - name: cargo build 54 | uses: actions-rs/cargo@v1 55 | with: 56 | command: build 57 | 58 | - name: cargo doc 59 | uses: actions-rs/cargo@v1 60 | with: 61 | command: doc 62 | args: --workspace --no-deps 63 | 64 | - name: cargo test 65 | uses: actions-rs/cargo@v1 66 | with: 67 | command: test 68 | 69 | clippy: 70 | name: Lint 71 | 72 | runs-on: ubuntu-latest 73 | 74 | steps: 75 | - name: git checkout 76 | uses: actions/checkout@v2 77 | 78 | - name: cache crates 79 | uses: actions/cache@v2 80 | with: 81 | path: | 82 | ~/.cargo/registry 83 | ~/.cargo/git 84 | target 85 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 86 | 87 | - name: rustup install 88 | uses: actions-rs/toolchain@v1 89 | with: 90 | toolchain: nightly 91 | profile: minimal 92 | override: true 93 | components: clippy 94 | 95 | - name: cargo clippy 96 | uses: actions-rs/clippy-check@v1 97 | with: 98 | token: ${{ secrets.GITHUB_TOKEN }} 99 | # TODO: all features 100 | # args: --all-features 101 | 102 | # TODO: run cargo fmt 103 | # - name: cargo fmt 104 | # uses: actions-rs/cargo@v1 105 | # with: 106 | # command: fmt 107 | # args: --all -- --check 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | out 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | ### stable features ### 2 | 3 | max_width = 80 4 | 5 | normalize_comments = true 6 | reorder_imports = true 7 | use_try_shorthand = true 8 | wrap_comments = true 9 | 10 | ### unstable features ## 11 | 12 | unstable_features = true 13 | 14 | struct_field_align_threshold = 64 15 | -------------------------------------------------------------------------------- /BUG-BOUNTY.md: -------------------------------------------------------------------------------- 1 | Serious about security 2 | ====================== 3 | 4 | Square recognizes the important contributions the security research community 5 | can make. We therefore encourage reporting security issues with the code 6 | contained in this repository. 7 | 8 | If you believe you have discovered a security vulnerability, please follow the 9 | guidelines at . 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | 'sudo_plugin', 4 | 'sudo_plugin-sys', 5 | 'sudo_pair', 6 | 7 | 'examples/raw_plugin_api', 8 | ] 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM rust:latest AS base 4 | 5 | ARG DEBIAN_FRONTEND=noninteractive 6 | 7 | RUN --mount=type=cache,target=/var/cache/apt \ 8 | apt-get update && \ 9 | apt-get dist-upgrade -y && \ 10 | apt-get install -y \ 11 | libclang1 \ 12 | sudo 13 | 14 | FROM base AS build 15 | 16 | # we depend upon: 17 | # * >= 1.32 for uniform module paths 18 | # * >= 1.36 for std::mem::MaybeUninit 19 | # * >= 1.38 for std::ptr::cast 20 | # * >= 1.52 for warn(rustdoc:all) 21 | ARG TOOLCHAIN 22 | ENV TOOLCHAIN=${TOOLCHAIN:-1.52} 23 | 24 | RUN --mount=type=cache,target=/tmp/cache/cargo \ 25 | --mount=type=cache,target=/tmp/cache/target,sharing=private \ 26 | rustup self update && \ 27 | rustup toolchain install $TOOLCHAIN && \ 28 | rustup default $TOOLCHAIN && \ 29 | rustup component add clippy 30 | 31 | ENV CARGO_HOME=/tmp/cache/cargo 32 | ENV CARGO_TARGET_DIR=/tmp/cache/target 33 | 34 | WORKDIR /srv/rust 35 | 36 | FROM build AS sudo_pair-deps 37 | 38 | RUN cargo new --lib sudo_plugin-sys 39 | RUN cargo new --lib sudo_plugin 40 | RUN cargo new --lib sudo_pair 41 | RUN cargo new --lib examples/raw_plugin_api 42 | 43 | COPY Cargo.toml . 44 | COPY sudo_plugin-sys/Cargo.toml ./sudo_plugin-sys 45 | COPY sudo_plugin-sys/build.rs ./sudo_plugin-sys 46 | COPY sudo_plugin-sys/src/bindings ./sudo_plugin-sys/src/bindings 47 | COPY sudo_plugin/Cargo.toml ./sudo_plugin 48 | COPY sudo_pair/Cargo.toml ./sudo_pair 49 | COPY examples/raw_plugin_api/Cargo.toml ./examples/raw_plugin_api 50 | 51 | RUN --mount=type=cache,target=${CARGO_HOME} \ 52 | --mount=type=cache,target=${CARGO_TARGET_DIR} \ 53 | cargo build 54 | 55 | FROM sudo_pair-deps AS sudo_pair 56 | 57 | ARG CARGOFLAGS 58 | ARG RUSTFLAGS="-A warnings -A unknown_lints --verbose" 59 | ARG RUSTDOCFLAGS 60 | 61 | # replace the dummy crates with the full project 62 | COPY . . 63 | 64 | FROM sudo_pair AS sudo_pair-build 65 | 66 | RUN --mount=type=cache,target=${CARGO_HOME} \ 67 | --mount=type=cache,target=${CARGO_TARGET_DIR} \ 68 | cargo build ${CARGOFLAGS} 69 | 70 | FROM sudo_pair-build AS sudo_pair-test 71 | 72 | RUN --mount=type=cache,target=${CARGO_HOME} \ 73 | --mount=type=cache,target=${CARGO_TARGET_DIR} \ 74 | cargo test ${CARGOFLAGS} 75 | 76 | FROM sudo_pair-build AS sudo_pair-lint 77 | 78 | RUN --mount=type=cache,target=${CARGO_HOME} \ 79 | --mount=type=cache,target=${CARGO_TARGET_DIR} \ 80 | cargo clippy ${CARGOFLAGS} 81 | 82 | FROM sudo_pair AS sudo_pair-sample 83 | 84 | RUN --mount=type=cache,target=/var/cache/apt \ 85 | apt-get install -y \ 86 | busybox-syslogd \ 87 | socat \ 88 | vim 89 | 90 | # copy the cached cargo build stages into the container 91 | RUN --mount=type=cache,target=${CARGO_HOME} \ 92 | --mount=type=cache,target=${CARGO_TARGET_DIR} \ 93 | cp -a ${CARGO_HOME} ~/.cargo && \ 94 | cp -a ${CARGO_TARGET_DIR} . 95 | 96 | ENV CARGO_HOME=~/.cargo 97 | ENV CARGO_TARGET_DIR=/srv/rust/target 98 | 99 | RUN make -C sample 100 | RUN make -C sample prefix= exec_prefix=/usr install 101 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./sudo_pair/README.md -------------------------------------------------------------------------------- /contrib/DEBIAN/conffiles: -------------------------------------------------------------------------------- 1 | /etc/sudo.conf_for_sudo_pair_sample 2 | -------------------------------------------------------------------------------- /contrib/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: sudo-pair 2 | Version: 1.2.1 3 | License: Apache-2.0 License 4 | Vendor: square 5 | Architecture: amd64 6 | Maintainer: fabio.germann@gmail.com 7 | Installed-Size: 1824 8 | Section: default 9 | Priority: extra 10 | Depends: socat 11 | Homepage: https://github.com/square/sudo_pair 12 | Description: Plugin for sudo that requires another human to approve and monitor privileged sudo sessions 13 | -------------------------------------------------------------------------------- /contrib/DEBIAN/md5sums: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sudo_pair/cf471074712acaf4f938b6845a2fb9b211c1da79/contrib/DEBIAN/md5sums -------------------------------------------------------------------------------- /contrib/DEBIAN/postinst: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | chmod 644 /usr/lib/sudo 4 | chown root:root /usr/lib/sudo 5 | 6 | chmod 755 /var/run/sudo_pair 7 | chown root:root /var/run/sudo_pair 8 | 9 | chmod 755 /usr/lib/sudo/libsudo_pair.so 10 | chown root:root /usr/lib/sudo/libsudo_pair.so 11 | 12 | chmod 755 /usr/bin/sudo_approve 13 | chown root:root /usr/bin/sudo_approve 14 | 15 | chmod 644 /etc/sudo.conf_for_sudo_pair_sample 16 | chown root:root /etc/sudo.conf_for_sudo_pair_sample 17 | 18 | chmod 644 /etc/sudo.prompt.user 19 | chown root:root /etc/sudo.prompt.user 20 | 21 | chmod 644 /etc/sudo.prompt.pair 22 | chown root:root /etc/sudo.prompt.pair 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /contrib/make_deb_pkg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | REPO_ROOT=$(git rev-parse --show-toplevel) 4 | CURRENT_DIR=$(pwd) 5 | 6 | # switch to root of repo 7 | cd "${REPO_ROOT}" 8 | 9 | # make release build 10 | cargo build --release 11 | 12 | DEB_TMPL_DIR="${REPO_ROOT}/contrib/DEBIAN" 13 | OUT_DIR="${REPO_ROOT}/out" 14 | DEB_PKG_DIR="${OUT_DIR}/debian" 15 | 16 | # create packagind directory 17 | mkdir -p ${DEB_PKG_DIR} 18 | 19 | # copy templates 20 | cp -R "${DEB_TMPL_DIR}" "${DEB_PKG_DIR}/" 21 | 22 | # shared library 23 | mkdir -p "${DEB_PKG_DIR}/usr/lib/sudo" 24 | cp "${REPO_ROOT}/target/release/libsudo_pair.so" "${DEB_PKG_DIR}/usr/lib/sudo" 25 | 26 | # socket directory 27 | mkdir -p "${DEB_PKG_DIR}/var/run/sudo_pair" 28 | 29 | # approval script 30 | mkdir -p "${DEB_PKG_DIR}/usr/bin" 31 | cp "${REPO_ROOT}/sample/bin/sudo_approve" "${DEB_PKG_DIR}/usr/bin/sudo_approve" 32 | 33 | # sudo.conf 34 | mkdir "${DEB_PKG_DIR}/etc" 35 | cp "${REPO_ROOT}/sample/etc/sudo.conf" "${DEB_PKG_DIR}/etc/sudo.conf_for_sudo_pair_sample" 36 | 37 | # prompts 38 | cp "${REPO_ROOT}/sample/etc/sudo.prompt.user" "${DEB_PKG_DIR}/etc/sudo.prompt.user" 39 | cp "${REPO_ROOT}/sample/etc/sudo.prompt.pair" "${DEB_PKG_DIR}/etc/sudo.prompt.pair" 40 | 41 | VERSION=$(cat "${DEB_TMPL_DIR}/control" | grep Version | cut -d ' ' -f 2) 42 | dpkg-deb -b "${DEB_PKG_DIR}" "${OUT_DIR}/sudo-pair-${VERSION}.deb" 43 | 44 | # switch back 45 | cd "${CURRENT_DIR}" 46 | -------------------------------------------------------------------------------- /contrib/sudo_pair.spec: -------------------------------------------------------------------------------- 1 | Name: sudo_pair 2 | Version: 1.0.0 3 | Release: 1 4 | Summary: Plugin for sudo that requires another human to approve and monitor privileged sudo sessions. 5 | Group: System Environment/Libraries 6 | License: Apache Software License 2.0 7 | Url: https://github.com/square/sudo_pair 8 | Source: https://github.com/square/sudo_pair/archive/sudo_pair-v%{version}.tar.gz 9 | 10 | BuildRoot: %{_tmppath}/%{name}-%{version}-build 11 | BuildRequires: cargo 12 | BuildRequires: clang-devel 13 | BuildRequires: git 14 | Requires: sudo >= 1.9 15 | 16 | %description 17 | Plugin for sudo that requires another human to approve and monitor privileged sudo sessions 18 | 19 | %global debug_package %{nil} 20 | 21 | %prep 22 | 23 | %setup -n sudo_pair-sudo_pair-%{version} 24 | 25 | %build 26 | cargo build --release 27 | 28 | %install 29 | mkdir -p %{buildroot}/usr/libexec/sudo 30 | %{__cp} target/release/libsudo_pair.so %{buildroot}/usr/libexec/sudo/ 31 | 32 | %clean 33 | rm -rf %{buildroot} 34 | 35 | %files 36 | /usr/libexec/sudo/libsudo_pair.so 37 | %doc README.md 38 | %doc sudo_pair/README.md 39 | %doc sample/etc/sudo.conf 40 | %doc sample/etc/sudo.prompt.pair 41 | %doc sample/etc/sudo.prompt.user 42 | %doc sample/bin/sudo_approve 43 | 44 | %changelog 45 | * Wed May 23 2018 - robert (at) meinit.nl 46 | - Initial release. 47 | * Fri Jul 3 2020 - robert (at) meinit.nl 48 | - Bump version to 1.0.0 49 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sudo_pair/cf471074712acaf4f938b6845a2fb9b211c1da79/demo.gif -------------------------------------------------------------------------------- /examples/raw_plugin_api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = 'deny_everything' 3 | version = '1.0.0' 4 | license = 'Apache-2.0' 5 | edition = '2018' 6 | 7 | authors = ['Stephen Touset '] 8 | description = 'sudo approval plugin that denies everything' 9 | 10 | homepage = 'https://github.com/square/sudo_pair/tree/master/examples/raw_plugin_api' 11 | repository = 'https://github.com/square/sudo_pair.git' 12 | readme = 'README.md' 13 | 14 | categories = ['command-line-utilities'] 15 | keywords = [ 'sudo', 'sudo-plugin'] 16 | 17 | [lib] 18 | name = 'deny_everything' 19 | crate-type = ['cdylib'] 20 | 21 | [dependencies] 22 | sudo_plugin-sys = { path = '../../sudo_plugin-sys' } 23 | 24 | -------------------------------------------------------------------------------- /examples/raw_plugin_api/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE-APACHE -------------------------------------------------------------------------------- /examples/raw_plugin_api/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | //! An example minimal sudo approval plugin that denies all actions. 16 | 17 | #![warn(future_incompatible)] 18 | #![warn(nonstandard_style)] 19 | #![warn(rust_2021_compatibility)] 20 | #![warn(rust_2018_compatibility)] 21 | #![warn(rust_2018_idioms)] 22 | #![warn(unused)] 23 | 24 | #![warn(bare_trait_objects)] 25 | #![warn(missing_copy_implementations)] 26 | #![warn(missing_debug_implementations)] 27 | #![warn(missing_docs)] 28 | #![warn(single_use_lifetimes)] 29 | #![warn(trivial_casts)] 30 | #![warn(trivial_numeric_casts)] 31 | #![warn(unreachable_pub)] 32 | #![warn(unstable_features)] 33 | #![warn(unused_import_braces)] 34 | #![warn(unused_lifetimes)] 35 | #![warn(unused_qualifications)] 36 | #![warn(unused_results)] 37 | #![warn(variant_size_differences)] 38 | 39 | #![warn(rustdoc::all)] 40 | 41 | #![warn(clippy::cargo)] 42 | #![warn(clippy::complexity)] 43 | #![warn(clippy::correctness)] 44 | #![warn(clippy::pedantic)] 45 | #![warn(clippy::perf)] 46 | #![warn(clippy::style)] 47 | 48 | use sudo_plugin_sys as sudo; 49 | use std::os::raw; 50 | 51 | /// Plugins must be exposed as `pub static mut` and tagged with the 52 | /// `no_mangle` attribute. The name of the variable it is assigned to will 53 | /// be the `symbol_name` to be used in [`sudo.conf`][man-sudo-conf]. 54 | /// 55 | /// The `no_mangle` attribute ensures that the exported symbol name is the 56 | /// same as its variable name. It also ensures that the item will be 57 | /// exported to the compiled library even though we're not using it directly 58 | /// ourselves. 59 | /// 60 | /// The `static mut` declaration is required to ensure that the plugin is 61 | /// placed in a single precise memory location in the compiled library (as 62 | /// opposed to `const`, which simply inlines the value wherever it's used). 63 | /// It must be declared `mut` so that the contents are not placed into 64 | /// read-only memory, as the sudo plugin interface may write values into 65 | /// the plugin (e.g., the `event_alloc` field). 66 | /// 67 | /// [man-sudo-conf]: https://www.sudo.ws/man/sudo.conf.man.html 68 | #[no_mangle] 69 | pub static mut deny_everything: sudo::approval_plugin = sudo::approval_plugin { 70 | open: Some(open), 71 | check: Some(check), 72 | show_version: Some(show_version), 73 | 74 | .. sudo::approval_plugin::empty() 75 | }; 76 | 77 | static mut SUDO_CONV: sudo::sudo_conv_t = None; 78 | static mut SUDO_PRINT: sudo::sudo_printf_t = None; 79 | static mut SUDO_API_VERSION: Option = None; 80 | 81 | const VERSION_MSG: *const raw::c_char = b"Deny Everything version 1.0\n\0".as_ptr().cast(); 82 | const ERRSTR: *const raw::c_char = b"deny_everything: denied\n\0" .as_ptr().cast(); 83 | 84 | unsafe extern "C" fn open( 85 | version: raw::c_uint, 86 | conversation: sudo::sudo_conv_t, 87 | sudo_printf: sudo::sudo_printf_t, 88 | _settings: *const *mut raw::c_char, 89 | _user_info: *const *mut raw::c_char, 90 | _submit_optind: raw::c_int, 91 | _submit_argv: *const *mut raw::c_char, 92 | _submit_envp: *const *mut raw::c_char, 93 | _plugin_options: *const *mut raw::c_char, 94 | _errstr: *mut *const raw::c_char, 95 | ) -> raw::c_int { 96 | SUDO_CONV = conversation; 97 | SUDO_PRINT = sudo_printf; 98 | SUDO_API_VERSION = Some(version); 99 | 100 | 1 101 | } 102 | 103 | unsafe extern "C" fn check( 104 | _command_info: *const *mut raw::c_char, 105 | _run_argv: *const *mut raw::c_char, 106 | _run_envp: *const *mut raw::c_char, 107 | errstr: *mut *const raw::c_char, 108 | ) -> raw::c_int { 109 | let print = match SUDO_PRINT { 110 | Some(f) => f, 111 | None => { return 0 }, 112 | }; 113 | 114 | #[allow(clippy::cast_possible_wrap)] 115 | let _ = (print)(sudo::SUDO_CONV_INFO_MSG as _, ERRSTR); 116 | 117 | if SUDO_API_VERSION.map(|v| v >= sudo::sudo_api_mkversion(1, 17)) == Some(true) { 118 | *errstr = ERRSTR; 119 | } 120 | 121 | 0 122 | } 123 | 124 | unsafe extern "C" fn show_version(_verbose: raw::c_int) -> raw::c_int { 125 | let print = match SUDO_PRINT { 126 | Some(f) => f, 127 | None => { return 0 }, 128 | }; 129 | 130 | #[allow(clippy::cast_possible_wrap)] 131 | let _ = (print)(sudo::SUDO_CONV_INFO_MSG as _, VERSION_MSG); 132 | 133 | 1 134 | } 135 | -------------------------------------------------------------------------------- /sample/Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := all 2 | 3 | SHELL = /bin/sh 4 | 5 | INSTALL := install 6 | INSTALL_PROGRAM := $(INSTALL) 7 | INSTALL_DATA := $(INSTALL) -m 644 8 | 9 | prefix := /usr/local 10 | exec_prefix := $(prefix) 11 | bindir := $(exec_prefix)/bin 12 | libdir := $(exec_prefix)/lib 13 | sysconfdir := $(prefix)/etc 14 | localstatedir := $(prefix)/var 15 | runstatedir := $(localstatedir)/run 16 | 17 | CARGO_TARGET_DIR ?= $(realpath ./target) 18 | CARGOFLAGS ?= --features slog/release_max_level_trace 19 | 20 | nogroup_gid := $(shell getent group nogroup | awk -F: '{print $$3}') 21 | 22 | .PHONY: all install 23 | 24 | all: $(CARGO_TARGET_DIR)/$(PROFILE)/libsudopair.so 25 | 26 | install: $(CARGO_TARGET_DIR)/$(PROFILE)/libsudopair.so 27 | $(INSTALL_DATA) -d $(DESTDIR)$(runstatedir)/sudo_pair 28 | 29 | $(INSTALL_PROGRAM) ./bin/sudo_approve $(DESTDIR)$(bindir) 30 | $(INSTALL_DATA) $(CARGO_TARGET_DIR)/release/libsudo_pair.so $(DESTDIR)$(libdir)/sudo/sudo_pair.so 31 | $(INSTALL_DATA) ./etc/sudo_pair.prompt.pair $(DESTDIR)$(sysconfdir) 32 | $(INSTALL_DATA) ./etc/sudo_pair.prompt.user $(DESTDIR)$(sysconfdir) 33 | $(INSTALL_DATA) -m440 ./etc/sudoers.d/sudo_pair $(DESTDIR)$(sysconfdir)/sudoers.d 34 | 35 | echo "Plugin sudo_pair sudo_pair.so gids_enforced=$(nogroup_gid)" >> $(DESTDIR)$(sysconfdir)/sudo.conf 36 | 37 | $(CARGO_TARGET_DIR)/$(PROFILE)/libsudopair.so: 38 | export CARGO_TARGET_DIR 39 | 40 | cargo build --verbose --manifest-path ../Cargo.toml --release $(CARGOFLAGS) 41 | 42 | # TODO: express rust dependencies 43 | # TODO: target as dylib/so 44 | # TODO: pre/post install command categories? 45 | -------------------------------------------------------------------------------- /sample/bin/sudo_approve: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright 2018 Square Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | # implied. See the License for the specific language governing 15 | # permissions and limitations under the License. 16 | 17 | set -o errexit # quit on first error 18 | set -o pipefail # quit on failures in pipes 19 | set -o nounset # quit on unset variables 20 | 21 | [[ ${TRACE:-} ]] && set -o xtrace # output subcommands if TRACE is set 22 | 23 | declare -r SUDO_SOCKET_PATH="/var/run/sudo_pair" 24 | 25 | pair() { 26 | declare -r socket="${1}" 27 | 28 | # restore TTY settings on exit 29 | # shellcheck disable=SC2064 30 | trap "stty $(stty -g)" EXIT 31 | 32 | # disable line-buffering and local echo, so the pairer doesn't 33 | # get confused that their typing in the shell isn't doing 34 | # anything 35 | stty cbreak -echo 36 | 37 | # send SIGINT on Ctrl-D 38 | stty intr "^D" 39 | 40 | clear 41 | 42 | # prompt the user to approve 43 | socat STDIO unix-connect:"${socket}" 44 | } 45 | 46 | usage() { 47 | echo "Usage: $(basename -- "$0") uid pid" 48 | exit 1 49 | } 50 | 51 | main() { 52 | declare -r socket_path="${1}" 53 | declare -ri uid="${2}" 54 | declare -ri pid="${3}" 55 | 56 | # if we're running this under `sudo`, we want to know the original 57 | # user's `uid` from `SUDO_UID`; if not, it's jsut their normal `uid` 58 | declare -i ruid 59 | ruid="${SUDO_UID:-$(id -u)}" 60 | declare -r ruid 61 | 62 | declare -r socket="${socket_path}/${uid}.${pid}.sock" 63 | 64 | declare -i socket_uid socket_gid 65 | socket_uid="$(stat -c '%u' "${socket}")" 66 | socket_gid="$(stat -c '%g' "${socket}")" 67 | declare -r socket_uid socket_gid 68 | 69 | declare socket_user socket_group socket_mode 70 | socket_user="$(getent passwd "${socket_uid}" | cut -d: -f1)" 71 | socket_group="$(getent group "${socket_gid}" | cut -d: -f1)" 72 | socket_mode="$(stat -c '%a' "${socket}")" 73 | declare -r socket_user socket_group socket_mode 74 | 75 | # if the user approving the command is the same as the user who 76 | # invoked `sudo` in the first place, abort 77 | # 78 | # another option would be to allow the session, but log it in a way 79 | # that it immediately pages oncall security engineers; such an 80 | # approach is useful in production systems in that it allows for a 81 | # in-case-of-fire-break-glass workaround so engineers can respond to 82 | # a outage in the middle of the night 83 | # 84 | # this responsibility will be moved into the plugin itself when time 85 | # allots 86 | if [[ "${uid}" -eq "${ruid}" ]]; then 87 | echo "Users may not approve their own sudo session" 88 | exit 1 89 | fi 90 | 91 | # if we can write: pair 92 | # if user-owner can write: sudo to them and try again 93 | # if group-owner can write: sudo to them and try again 94 | # if none, die 95 | if [ -w "${socket}" ]; then 96 | pair "${socket}" 97 | elif [[ $(( 8#${socket_mode} & 8#200 )) -ne 0 ]]; then 98 | sudo -u "${socket_user}" "${0}" "${uid}" "${pid}" 99 | elif [[ $(( 8#${socket_mode} & 8#020 )) -ne 0 ]]; then 100 | sudo -g "${socket_group}" "${0}" "${uid}" "${pid}" 101 | else 102 | echo "The socket for this sudo session is neither user- nor group-writable." 103 | exit 2 104 | fi 105 | } 106 | 107 | case "$#" in 108 | 2) main "${SUDO_SOCKET_PATH}" "$1" "$2" ;; 109 | *) usage ;; 110 | esac 111 | -------------------------------------------------------------------------------- /sample/etc/sudo.conf: -------------------------------------------------------------------------------- 1 | # `sudo_pair` is the name of the plugin's entry point inside of the 2 | # shared object and should not be changed. 3 | # 4 | # `sudo_pair.so` is the name of the plugin's shared library, relative 5 | # to sudo's plugin directory (typically `/usr/libexec/sudo` or 6 | # `/usr/local/libexec/sudo`). 7 | # 8 | # Following these two required entries is the list of plugin-specific 9 | # options. 10 | 11 | Plugin sudo_pair sudo_pair.so gids_enforced= 12 | -------------------------------------------------------------------------------- /sample/etc/sudo_pair.prompt.pair: -------------------------------------------------------------------------------- 1 | You have been asked to approve and monitor the following sudo session: 2 | 3 | <%U@%h:%d $ > %C 4 | 5 | Once approved, this terminal will mirror all output from the active sudo session until its completion. 6 | 7 | Closing this terminal, losing your network connection to this host, or explicitly ending the session by typing will cause the command being run under elevated privileges to terminate immediately. 8 | 9 | Approve? y/n [n]: -------------------------------------------------------------------------------- /sample/etc/sudo_pair.prompt.user: -------------------------------------------------------------------------------- 1 | Due to security and compliance requirements, this `sudo` session will require approval and monitoring. 2 | 3 | To continue, another human must run: 4 | 5 | docker exec -it %h '%B %u %p' 6 | 7 | If a suitable human is not available and you have an immediate and urgent need to run this command, you may run the above command to approve your own session. Note that doing so will immediately page an oncall security engineer, so this capability should only be used in the event of an emergency. 8 | -------------------------------------------------------------------------------- /sample/etc/sudoers.d/sudo_pair: -------------------------------------------------------------------------------- 1 | nobody ALL = (: games) NOPASSWD: LOG_OUTPUT: ALL 2 | -------------------------------------------------------------------------------- /sudo_pair/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## Added 11 | - Structured logging to syslog and journald through the `slog` crate. Enabled 12 | through an optional feature. 13 | - Support for automatically changing the window size of the pair terminal. 14 | Requires sudo 1.8.21 or greater. 15 | 16 | ## Changed 17 | - Rewritten to work on the v2 sudo_plugin API. 18 | 19 | ## [1.0.0] - 2020-03-26 20 | 21 | ### Fixed 22 | - The `-u` and `-g` flags can now both be used in exempt sessions. 23 | 24 | ### Changed 25 | - Builds using Rust 2018 26 | - Errors are handled through the `failure` crate rather than `error-chain`. 27 | 28 | ## [0.11.1] - 2018-06-08 29 | 30 | ### Fixed 31 | - The approval command is once again implicitly whitelisted, this was 32 | unintentionally removed when adding support for obeying `log_output` hinting 33 | in `/etc/sudoers`. 34 | 35 | ## [0.11.0] - 2018-06-06 36 | 37 | ### Added 38 | - Now obeys the `log_output` setting from `/etc/sudoers`. However, this renders 39 | this plugin completely nonfunctional unless this setting is enabled (or 40 | individual commands are opted in with the `LOG_OUTPUT:` tag). 41 | 42 | ### Removed 43 | - Removed the short-lived `whitelist` setting in favor of simply honoring 44 | `log_output`. 45 | 46 | ## [0.10.0] - 2018-06-05 47 | 48 | ### Added 49 | - New `whitelist` plugin option allows for naming binaries to be exempt from 50 | requiring a pair. 51 | 52 | ### Changed 53 | - No longer fails to build on warnings, unless being run in CI 54 | 55 | ## [0.9.2] - 2018-05-18 56 | 57 | ### Added 58 | - No longer forbids redirecting standard out and standard error 59 | 60 | ### Changed 61 | - Prompt is rendered directly to the user's TTY when possible 62 | 63 | ### Fixed 64 | - Output sent to the plugin printf function is sent with `write_all` for 65 | technical correctness (although AFAICT this is unnecessary in practice) 66 | 67 | ## [0.9.1] - 2018-05-08 68 | 69 | ### Security 70 | - Ensure approval sockets aren't created with the primary group of the new user 71 | - Print all the arguments passed to the command being `sudo`ed (thanks 72 | [`/u/__xor__`](https://www.reddit.com/r/rust/comments/8hppka/sudo_pair_090_released/dymsev8/)) 73 | 74 | ### Fixed 75 | - Rolled back the minimum plugin API version to 1.9; it was mistakenly bumped to 1.12 when support for 1.12 was added 76 | 77 | ## 0.9.0 - 2018-05-07 78 | 79 | ### Added 80 | - First public release, stabilization pending feedback from the community 81 | 82 | [Unreleased]: https://github.com/square/sudo_pair/compare/sudo_pair-v1.0.0...master 83 | [1.0.0]: https://github.com/square/sudo_pair/compare/sudo_pair-v0.11.1...sudo_pair-v1.0.0 84 | [0.11.1]: https://github.com/square/sudo_pair/compare/sudo_pair-v0.11.0...sudo_pair-v0.11.1 85 | [0.11.0]: https://github.com/square/sudo_pair/compare/sudo_pair-v0.10.0...sudo_pair-v0.11.0 86 | [0.10.0]: https://github.com/square/sudo_pair/compare/sudo_pair-v0.9.2...sudo_pair-v0.10.0 87 | [0.9.2]: https://github.com/square/sudo_pair/compare/sudo_pair-v0.9.1...sudo_pair-v0.9.2 88 | [0.9.1]: https://github.com/square/sudo_pair/compare/sudo_pair-v0.9.0...sudo_pair-v0.9.1 89 | -------------------------------------------------------------------------------- /sudo_pair/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = 'sudo_pair' 3 | version = '1.0.0' 4 | license = 'Apache-2.0' 5 | edition = '2018' 6 | 7 | authors = ['Stephen Touset '] 8 | description = 'sudo IO-plugin to require a live human pair' 9 | 10 | homepage = 'https://github.com/square/sudo_pair' 11 | repository = 'https://github.com/square/sudo_pair.git' 12 | readme = '../README.md' 13 | 14 | categories = [ 'command-line-utilities' ] 15 | keywords = [ 'sudo', 'sudo-plugin', 'dual-control', 'sox' ] 16 | 17 | [lib] 18 | name = 'sudo_pair' 19 | crate-type = ['cdylib'] 20 | 21 | [features] 22 | default = ['syslog', 'change_winsize'] 23 | change_winsize = [] 24 | journald = ['slog-journald'] 25 | syslog = ['slog-syslog'] 26 | 27 | [dependencies] 28 | libc = '0.2.70' 29 | failure = '0.1.8' 30 | slog = '2.5' 31 | sudo_plugin = { version = '1.2', path = '../sudo_plugin' } 32 | 33 | slog-journald = { version = '2.1', optional = true } 34 | slog-syslog = { version = '0.12.0', optional = true } 35 | -------------------------------------------------------------------------------- /sudo_pair/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /sudo_pair/README.md: -------------------------------------------------------------------------------- 1 | sudo_pair 2 | ========= 3 | 4 | [![Build Status](https://travis-ci.org/square/sudo_pair.svg?branch=master)](https://travis-ci.org/square/sudo_pair) 5 | [![Latest Version](https://img.shields.io/github/release/square/sudo_pair.svg)](https://github.com/square/sudo_pair/releases) 6 | [![License](https://img.shields.io/github/license/square/sudo_pair.svg)](https://github.com/square/sudo_pair) 7 | 8 | `sudo_pair` is a [plugin for sudo][sudo_plugin_man] that requires another 9 | human to approve and monitor privileged sudo sessions. 10 | 11 |

12 | a demonstrated sudo_pair session 13 |

14 | 15 | ## About 16 | 17 | `sudo` is used by engineers daily to run commands as privileged users. 18 | But on some sensitive systems, you really want to ensure that no 19 | individual can act entirely autonomously. At Square, this includes 20 | applications that manage our internal access-control systems, store 21 | accounting ledgers, or even move around real money. This plugin allows 22 | us to ensure that no user can act entirely on their own authority within 23 | these systems. 24 | 25 | This plugin and its components are still in prerelease, as we want to 26 | get feedback from the open-source community before officially releasing 27 | 1.0. 28 | 29 | ## Installation 30 | 31 | ### WARNING: Misconfiguring sudo can lock you out of your machine. Test this in a throwaway environment. 32 | 33 | For now, `sudo_pair` must be compiled from source. It is a standard 34 | Rust project, and the following should suffice to build it on any recent 35 | version of Rust: 36 | 37 | ```sh 38 | git clone https://github.com/square/sudo_pair.git 39 | cd sudo_pair 40 | cargo build --release 41 | ``` 42 | 43 | Once built, the plugin itself will need to be installed in a place where 44 | `sudo` can find it. Generally this is under `/usr/libexec/sudo` (on 45 | macOS hosts it's `/usr/local/libexec/sudo`). An appropriate approval 46 | script must be installed into the `PATH`. A directory must be created 47 | for `sudo_pair` to manage the sockets it uses for communication between 48 | plugin and client. And finally, `sudo` must be configured to load and 49 | use the plugin. 50 | 51 | ```sh 52 | # WARNING: these files may not be set up in a way that is suitable 53 | # for your system. Proceed only on a throwaway host. 54 | 55 | # install the plugin shared library 56 | install -o root -g root -m 0644 ./target/release/libsudopair.dylib /usr/libexec/sudo 57 | 58 | # create a socket directory 59 | install -o root -g root -m 0644 -d /var/run/sudo_pair 60 | 61 | # install the approval script; as currently configured, it denies access 62 | # to users approving their own sudo session and may lock you out 63 | install -o root -g root -m 0755 ./sample/bin/sudo_approve /usr/bin/sudo_approve 64 | 65 | # your `/etc/sudo.conf` may already have entries necessary for sudo to 66 | # function correctly; if this is the case, the two files will need to be 67 | # merged 68 | install -o root -g root -m 0644 ./sample/etc/sudo.conf /etc/sudo.conf 69 | 70 | # if these prompts don't work for you, they're configurable via a simple 71 | # templating language explained later in the README 72 | install -o root -g root -m 0644 ./sample/etc/sudo.prompt.user /etc/sudo.prompt.user 73 | install -o root -g root -m 0644 ./sample/etc/sudo.prompt.pair /etc/sudo.prompt.pair 74 | ``` 75 | 76 | This only places the plugin files into their expected locations. The plugin 77 | will not be enabled yet until you follow the [configuration](#configuration) 78 | steps below. 79 | 80 | ## Configuration 81 | 82 | ### `/etc/sudoers` 83 | 84 | By default, `/etc/sudoers` will not tell logging plugins to log output for 85 | any commands. You will need to enable this by either telling `sudo` to enable 86 | logging for all commands (and opt out any commands you wish to bypass pairing 87 | for) or by opting individual commands into logging. 88 | 89 | Example (default to log, opt out of individual commands): 90 | 91 | ``` 92 | Defaults log_output 93 | 94 | %wheel ALL = (ALL) NOLOG_OUTPUT: /bin/cat, /bin/ls 95 | ``` 96 | 97 | Example (opt into individual commands) 98 | 99 | ``` 100 | %wheel ALL = (ALL) LOG_OUTPUT: /usr/bin/visudo 101 | ``` 102 | 103 | ### `/etc/sudo.conf` 104 | 105 | The plugin can be provided several options to modify its behavior. These 106 | options are provided to the plugin by adding them to the end of the 107 | `Plugin` line in `/etc/sudo.conf`. 108 | 109 | Example: 110 | 111 | ``` 112 | Plugin sudo_pair sudo_pair.so socket_dir=/var/tmp/sudo_pair gids_exempted=42,109 113 | ``` 114 | 115 | The full list of options are as follows: 116 | 117 | * `binary_path` (default: `/usr/bin/sudo_approve`) 118 | 119 | This is the location of the approval binary. The approval command itself needs to run under the privileges of the destination user or group, and this is done so using sudo, so it must be exempted from requiring its own pair approval. 120 | 121 | * `user_prompt_path` (default: `/etc/sudo_pair.prompt.user`) 122 | 123 | This is the location of the prompt template to display to the user invoking sudo; if no template is found at this location, an extremely minimal default will be printed. See the [Prompts](#prompts) section for more details. 124 | 125 | * `pair_prompt_path` (default: `/etc/sudo_pair.prompt.pair`) 126 | 127 | This is the location of the prompt template to display to the user being asked to approve the sudo session; if no template is found at this location, an extremely minimal default will be printed. See the [Prompts](#prompts) section for more details. 128 | 129 | * `socket_dir` (default: `/var/run/sudo_pair`) 130 | 131 | This is the path where this plugin will store sockets for sessions that are pending approval. This directory must be owned by root and only writable by root, or the plugin will abort. 132 | 133 | * `gids_enforced` (default: `0`) 134 | 135 | This is a comma-separated list of gids that sudo_pair will gate access to. If a user is `sudo`ing to a user that is a member of one of these groups, they will be required to have a pair approve their session. 136 | 137 | * `gids_exempted` (default: none) 138 | 139 | This is a comma-separated list of gids whose users will be exempted from the requirements of sudo_pair. Note that this is not the opposite of the `gids_enforced` flag. Whereas `gids_enforced` gates access *to* groups, `gids_exempted` exempts users sudoing *from* groups. For instance, this setting can be used to ensure that oncall sysadmins can respond to outages without needing to find a pair. 140 | 141 | Note that root is *always* exempt. 142 | 143 | ## Prompts 144 | 145 | This plugin allows you to configure the prompts that are displayed to 146 | both users being asked to find a pair and users being asked to approve 147 | another user's `sudo` session. If prompts aren't 148 | [configured](#configuration) (or can't be found on the filesystem), 149 | extremely minimal ones are provided as a default. 150 | 151 | The contents of the prompt files are raw bytes that should be printed to 152 | the user's terminal. This allows fun things like terminal processing of 153 | ANSI escape codes for coloration, resizing terminals, and setting window 154 | titles, all of which are (ab)used in the sample prompts provided. 155 | 156 | These prompts also [implement](src/template.rs) a simple `%`-escaped 157 | templating language. Any known directive preceded by a `%` character is 158 | replaced by an expansion, and anything else is treated as a literal 159 | (e.g., `%%` is a literal `%`, and `%a` is a literal `a`). 160 | 161 | Available expansions: 162 | 163 | * `%b`: the name of the appoval _b_inary 164 | * `%B`: the full path to the approval _B_inary 165 | * `%C`: the full _C_ommand `sudo` was invoked as (recreated as best-effort) 166 | * `%d`: the cw_d_ of the command being run under `sudo` 167 | * `%h`: the _h_ostname of the machine `sudo` is being executed on 168 | * `%H`: the _H_eight of the invoking user's terminal, in rows 169 | * `%g`: the real _g_id of the user invoking `sudo` 170 | * `%p`: the _p_id of this `sudo` process 171 | * `%u`: the real _u_id of the user invoking `sudo` 172 | * `%U`: the _U_sername of the user running `sudo` 173 | * `%W`: the _W_idth of the invoking user's terminal, in columns 174 | 175 | ## Approval Scripts 176 | 177 | The [provided approval script](sample/bin/sudo_approve) is just a small 178 | (but complete) example. As much functionality as possible has been moved 179 | into the plugin, with one (important, temporary) exception: currently, 180 | the script must verify that the user approving a `sudo` session is not 181 | the user who is requesting the session. 182 | 183 | Other than that, the only thing required of the "protocol" is to: 184 | 185 | * connect to a socket (as either the user or group being `sudo`ed to) 186 | * wire up the socket's input and output to the user's STDIN and STDOUT 187 | * send a `y` to approve, or anything else to decline 188 | * close the socket to terminate the session 189 | 190 | As it turns out, you can pretty much just do this with `socat`: 191 | 192 | ```sh 193 | socat STDIO /path/to/socket 194 | ``` 195 | 196 | The script included with this project isn't much more than this. It 197 | performs a few extra niceties (implicitly `sudo`s if necessary, turns 198 | off terminal echo, disables Ctrl-C, and kills the session on Ctrl-D), 199 | but not much more. Ctrl-C was disabled so a user who's forgotten that 200 | this terminal is being used to monitor another user's session doesn't 201 | instinctively kill it with Ctrl-C. 202 | 203 | ## Limitations 204 | 205 | Sessions under `sudo_pair` can't be piped to. 206 | 207 | Allowing piped data to standard input, as far as I can tell, likely 208 | results in a complete bypass of the security model here. Commands can 209 | often accept input on `stdin`, and there's no reasonable way to show 210 | this information to the pair. 211 | 212 | ## Security Model 213 | 214 | This plugin allows users to `sudo -u ${user}` to become a user or 215 | `sudo -g ${group}` to gain an additional group. 216 | 217 | When a user does this, a socket is created that is owned and only 218 | writable by `${user}` (or `${group}`). In order to connect to that 219 | socket, the approver must be able to write to files as that `${user}` 220 | (or `${group}`). In other words, they need to be [on the other side of 221 | the airtight hatchway][airtight-hatchway]. In practical terms, this 222 | means the approver needs to also be able to `sudo` to that user or 223 | group. 224 | 225 | To facilitate this, the plugin exempts the approval script from the 226 | requirement to have a pair. And the sample approval script automatically 227 | detects the user or group you need to become and runs `sudo -u ${user}` 228 | (or `sudo -g ${group}`) implicitly. 229 | 230 | As a concrete example, these are the sockets opened for `sudo -u root`, 231 | `sudo -u nobody`, and `sudo -g sys`: 232 | 233 | ``` 234 | drwxr-xr-x 3 root wheel 96 May 8 09:17 . 235 | s-w------- 1 root wheel 0 May 8 09:16 1882.29664.sock # sudo -u root 236 | s-w------- 1 nobody wheel 0 May 8 09:17 1882.29921.sock # sudo -u nobody 237 | s----w---- 1 root sys 0 May 8 09:18 1882.29994.sock # sudo -g sys 238 | ``` 239 | 240 | The only people who can approve a `sudo` session to a user or group must 241 | *also* be able to `sudo` as that user or group. 242 | 243 | Due to limitations of the POSIX filesystem permission model, a user may 244 | sudo to a new user (and gain its groups) or sudo to a new group 245 | (preserving their current user), but not both simultaneously. 246 | 247 | ## Project Layout 248 | 249 | This project is composed of three Rust crates: 250 | 251 | * [`sudo_plugin-sys`](sudo_plugin-sys): raw Rust FFI bindings to the [`sudo_plugin(8)`][sudo_plugin_man] interface 252 | * [`sudo_plugin`](sudo_plugin): a set of Rust structs and macros to simplify writing plugins 253 | * [`sudo_pair`](sudo_pair): the implementation of this plugin 254 | 255 | ## Dependencies 256 | 257 | Given the security-sensitive nature of this project, it is an explicit 258 | goal to have a minimal set of dependencies. Currently, those are: 259 | 260 | * [rust-lang/libc][libc] 261 | * [rust-lang-nursery/rust-bindgen][bindgen] 262 | * [rust-lang-nursery/failure][failure] 263 | * [dtolnay/thiserror][thiserror] 264 | 265 | ## Contributions 266 | 267 | Contributions are welcome! This project should hopefully be small 268 | (~500loc for the plugin itself, ~1kloc for the wrappers around writing 269 | plugins) and well-documented enough for others to participate without 270 | difficulty. 271 | 272 | Pick a [TODO](sudo_pair/src/lib.rs) and get started! 273 | 274 | ## Bugs 275 | 276 | Please report non-security issues on the GitHub tracker. Security issues 277 | are covered by Square's [bug bounty program](BUG-BOUNTY.md). 278 | 279 | ## License 280 | 281 | `sudo_pair` is distributed under the terms of the Apache License 282 | (Version 2.0). 283 | 284 | See [LICENSE-APACHE](LICENSE-APACHE) for details. 285 | 286 | [sudo_plugin_man]: https://www.sudo.ws/man/1.8.22/sudo_plugin.man.html 287 | [libc]: https://github.com/rust-lang/libc 288 | [bindgen]: https://github.com/rust-lang-nursery/rust-bindgen 289 | [failure]: https://github.com/rust-lang-nursery/failure 290 | [thiserror]: https://github.com/dtolnay/thiserror 291 | [airtight-hatchway]: http://web.archive.org/web/20190119055125/https://blogs.msdn.microsoft.com/oldnewthing/20060508-22/?p=31283 292 | -------------------------------------------------------------------------------- /sudo_pair/src/errors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use std::fmt::{Display, Formatter, Result as FmtResult}; 16 | use std::result::Result as StdResult; 17 | use std::error::Error as StdError; 18 | 19 | use failure::Context; 20 | 21 | use sudo_plugin::prelude::{Error as PluginError, OpenStatus, LogStatus}; 22 | 23 | pub(crate) type Result = StdResult; 24 | 25 | #[derive(Clone, Eq, PartialEq, Debug)] 26 | pub(crate) enum ErrorKind { 27 | CommunicationError, 28 | SessionDeclined, 29 | SessionTerminated, 30 | StdinRedirected, 31 | SudoToUserAndGroup, 32 | 33 | PluginError(PluginError), 34 | } 35 | 36 | impl ErrorKind { 37 | fn as_str(&self) -> &'static str { 38 | match self { 39 | ErrorKind::CommunicationError => "couldn't establish communications with the pair", 40 | ErrorKind::SessionDeclined => "pair declined the session", 41 | ErrorKind::SessionTerminated => "pair ended the session", 42 | ErrorKind::StdinRedirected => "redirection of stdin to paired sessions is prohibited", 43 | ErrorKind::SudoToUserAndGroup => "the -u and -g options may not both be specified", 44 | 45 | ErrorKind::PluginError(_) => "the plugin failed to initialize", 46 | } 47 | } 48 | } 49 | 50 | impl Display for ErrorKind { 51 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 52 | self.clone().as_str().fmt(f) 53 | } 54 | } 55 | 56 | #[derive(Debug)] 57 | pub(crate) struct Error { 58 | inner: Context, 59 | } 60 | 61 | impl Display for Error { 62 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 63 | self.inner.fmt(f) 64 | } 65 | } 66 | 67 | impl From for Error { 68 | fn from(kind: ErrorKind) -> Self { 69 | Self::from(Context::new(kind)) 70 | } 71 | } 72 | 73 | impl From> for Error { 74 | fn from(inner: Context) -> Self { 75 | Self { inner } 76 | } 77 | } 78 | 79 | impl From for OpenStatus { 80 | fn from(_: Error) -> Self { 81 | OpenStatus::Deny 82 | } 83 | } 84 | 85 | impl From for LogStatus { 86 | fn from(_: Error) -> Self { 87 | LogStatus::Deny 88 | } 89 | } 90 | 91 | impl From for Error { 92 | fn from(err: PluginError) -> Self { 93 | ErrorKind::PluginError(err).into() 94 | } 95 | } 96 | 97 | impl StdError for Error { } 98 | -------------------------------------------------------------------------------- /sudo_pair/src/socket.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // these warnings are unavoidable with names like `uid` and `gid`, and 16 | // such names are natural to use for this problem domain so should not 17 | // be avoided 18 | #![allow(clippy::similar_names)] 19 | 20 | use std::ffi::CString; 21 | use std::fs; 22 | use std::io::{Read, Write, Result, Error, ErrorKind}; 23 | use std::net::Shutdown; 24 | use std::os::unix::prelude::*; 25 | use std::os::unix::net::{UnixListener, UnixStream}; 26 | use std::mem; 27 | use std::path::Path; 28 | use std::ptr; 29 | 30 | use libc::{self, gid_t, mode_t, uid_t}; 31 | 32 | #[derive(Debug)] 33 | pub(crate) struct Socket { 34 | socket: UnixStream, 35 | } 36 | 37 | impl Socket { 38 | pub(crate) fn open>( 39 | path: P, 40 | uid: uid_t, 41 | gid: gid_t, 42 | mode: mode_t, 43 | ) -> Result { 44 | let path = path.as_ref(); 45 | 46 | Self::enforce_ownership(path)?; 47 | 48 | // if the path already exists as a socket, make a best-effort 49 | // attempt at unlinking it 50 | Self::unlink(path)?; 51 | 52 | // by default, ensure no permissions on the created socket since 53 | // we're going to customize them immediately afterward 54 | let umask = unsafe { 55 | libc::umask(libc::S_IRWXU | libc::S_IRWXG | libc::S_IRWXO) 56 | }; 57 | 58 | let socket = UnixListener::bind(&path).and_then(|listener| { 59 | let cpath = CString::new( 60 | path.as_os_str().as_bytes() 61 | )?; 62 | 63 | unsafe { 64 | if libc::chown(cpath.as_ptr(), uid, gid) == -1 { 65 | return Err(Error::last_os_error()); 66 | }; 67 | 68 | if libc::chmod(cpath.as_ptr(), mode) == -1 { 69 | return Err(Error::last_os_error()); 70 | } 71 | 72 | let fd = listener.as_raw_fd(); 73 | let mut readfds = mem::MaybeUninit::::uninit(); 74 | 75 | libc::FD_ZERO(readfds.as_mut_ptr()); 76 | 77 | let mut readfds = readfds.assume_init(); 78 | 79 | libc::FD_SET(fd, &mut readfds); 80 | 81 | // rust automatically wraps the `accept()` function in a 82 | // loop that retries on SIGINT, so we have to get 83 | // creative here and `select(2)` ourselves if we want 84 | // Ctrl-C to interrupt the process 85 | match libc::select( 86 | fd + 1, // this must be greater than the fd's int value 87 | &mut readfds, 88 | ptr::null_mut(), 89 | ptr::null_mut(), 90 | ptr::null_mut(), 91 | ) { 92 | 1 => (), 93 | -1 => return Err(Error::last_os_error()), 94 | 0 => unreachable!("`select` returned 0 even though no timeout was set"), 95 | _ => unreachable!("`select` indicated that more than 1 fd is ready"), 96 | }; 97 | 98 | // as a sanity check, confirm that the fd we're going to 99 | // `accept` is the one that `select` says is ready 100 | if !libc::FD_ISSET(fd, &readfds) { 101 | unreachable!("`select` returned an unexpected file descriptor"); 102 | } 103 | } 104 | 105 | listener.accept().map(|connection| { 106 | Self { socket: connection.0 } 107 | }) 108 | }); 109 | 110 | // once the connection has been made (or aborted due to ctrl-c), 111 | // we don't need the socket to remain on the filesystem 112 | // 113 | // we ignore the result of this operation (instead of returning 114 | // the error because a) any error from the previous operation is 115 | // of higher importance (we didn't return the error immediately 116 | // because we want to unlink the socket regardless) and b) it's 117 | // more important to continue the sudo session than to worry 118 | // about filesystem janitorial work 119 | let _ = Self::unlink(path); 120 | 121 | // restore the process' original umask 122 | let _ = unsafe { libc::umask(umask) }; 123 | 124 | socket 125 | } 126 | 127 | pub(crate) fn close(&mut self) -> Result<()> { 128 | self.socket.shutdown(Shutdown::Both) 129 | } 130 | 131 | fn unlink(path: &Path) -> Result<()> { 132 | match fs::metadata(&path).map(|md| md.file_type().is_socket()) { 133 | // file exists, is a socket; delete it 134 | Ok(true) => fs::remove_file(path), 135 | 136 | // file exists, is not a socket; abort 137 | Ok(false) => Err(Error::new( 138 | ErrorKind::AlreadyExists, 139 | format!( 140 | "{} exists and is not a socket", 141 | path.to_string_lossy() 142 | ), 143 | )), 144 | 145 | // file doesn't exist; nothing to do 146 | _ => Ok(()), 147 | } 148 | } 149 | 150 | fn enforce_ownership(path: &Path) -> Result<()> { 151 | let parent = path.parent().ok_or_else(|| { 152 | Error::new(ErrorKind::AlreadyExists, format!( 153 | "couldn't determine permissions of the parent directory for {}", 154 | path.to_string_lossy() 155 | )) 156 | })?; 157 | 158 | let parent = CString::new( 159 | parent.as_os_str().as_bytes() 160 | )?; 161 | 162 | unsafe { 163 | let mut stat = mem::MaybeUninit::::uninit(); 164 | 165 | if libc::stat( 166 | parent.as_ptr(), 167 | stat.as_mut_ptr() 168 | ) == -1 { 169 | return Err(Error::last_os_error()); 170 | } 171 | 172 | let stat = stat.assume_init(); 173 | 174 | if stat.st_mode & libc::S_IFDIR == 0 { 175 | return Err(Error::new(ErrorKind::Other, format!( 176 | "the socket path {} is not a directory", 177 | parent.to_string_lossy(), 178 | ))); 179 | } 180 | 181 | if stat.st_uid != libc::geteuid() { 182 | return Err(Error::new(ErrorKind::Other, format!( 183 | "the socket directory {} is not owned by root", 184 | parent.to_string_lossy(), 185 | ))); 186 | } 187 | 188 | // TODO: temporarily disabled while I relearn everything I 189 | // know about POSIX filesystem ownership. 190 | // 191 | // All of this is to the best of my (current) understanding. 192 | // On Linux, new files are created with their uid and gid 193 | // set to the euid and egid of the process creating them. On 194 | // Darwin (and probably other BSDs), they are created with 195 | // the euid of the process creating them, but their egid is 196 | // that of the directory they're being created in (which is 197 | // typically behavior on Linux only if the setgid is enabled 198 | // on the directory). 199 | // 200 | // So I'd like to ensure the file is owned by root's primary 201 | // group, so that the created sockets don't inherit a group 202 | // that unprivileged users are in. But I'd first have to 203 | // actually figure out the primary group for my `euid` 204 | // (sudo is run *setuid*, which doesn't change the `egid`), 205 | // and then I'd have to... I don't know, check that nobody 206 | // else is in it? That doesn't seem like a lot of ROI on my 207 | // effort. So for now I'll just check that the group doesn't 208 | // have any permissions to this directory. 209 | // 210 | // if stat.st_gid != libc::getegid() { 211 | // return Err(Error::new(ErrorKind::Other, format!( 212 | // "the socket directory {} is not owned by root's group", 213 | // parent.to_string_lossy(), 214 | // ))); 215 | // } 216 | 217 | if stat.st_mode & (libc::S_IWGRP | libc::S_IWOTH) != 0 { 218 | return Err(Error::new(ErrorKind::Other, format!( 219 | "the socket directory {} has insecure permissions", 220 | parent.to_string_lossy(), 221 | ))); 222 | } 223 | } 224 | 225 | Ok(()) 226 | } 227 | } 228 | 229 | impl Drop for Socket { 230 | fn drop(&mut self) { 231 | let _ = self.close(); 232 | } 233 | } 234 | 235 | impl Read for Socket { 236 | fn read(&mut self, buf: &mut [u8]) -> Result { 237 | // read() will block until someone writes on the other side 238 | // of the socket, so we ensure that the signal handler for 239 | // Ctrl-C aborts the read instead of restarting it 240 | // automatically 241 | ctrl_c_aborts_syscalls(|| self.socket.read(buf) )? 242 | } 243 | } 244 | 245 | impl Write for Socket { 246 | fn write(&mut self, buf: &[u8]) -> Result { 247 | ctrl_c_aborts_syscalls(|| self.socket.write(buf) )? 248 | } 249 | 250 | fn flush(&mut self) -> Result<()> { 251 | ctrl_c_aborts_syscalls(|| self.socket.flush() )? 252 | } 253 | } 254 | 255 | /// Sets up a handler for Ctrl-C (SIGINT) that's a no-op, but with the 256 | /// `SA_RESTART` flag disabled, for the duration of the passed function 257 | /// call. 258 | /// 259 | /// Disabling `SA_RESTART` ensures that blocking calls like `accept(2)` 260 | /// will be terminated upon receipt on the signal instead of 261 | /// automatically resuming. 262 | fn ctrl_c_aborts_syscalls(func: F) -> Result 263 | where F: FnOnce() -> T 264 | { 265 | unsafe { 266 | let mut sigaction_old = mem::MaybeUninit::::uninit(); 267 | let sigaction_null = ::std::ptr::null_mut(); 268 | 269 | // retrieve the existing handler 270 | sigaction(libc::SIGINT, sigaction_null, sigaction_old.as_mut_ptr())?; 271 | 272 | let sigaction_old = sigaction_old.assume_init(); 273 | 274 | // copy the old handler, but mask out SA_RESTART 275 | let mut sigaction_new = sigaction_old; 276 | sigaction_new.sa_flags &= !libc::SA_RESTART; 277 | 278 | // install the new handler 279 | sigaction(libc::SIGINT, &sigaction_new, sigaction_null)?; 280 | 281 | let result = func(); 282 | 283 | // reinstall the old handler 284 | sigaction(libc::SIGINT, &sigaction_old, sigaction_null)?; 285 | 286 | Ok(result) 287 | } 288 | } 289 | 290 | /// Installs the new handler for the signal identified by `sig` if `new` 291 | /// is non-null. Returns the preexisting handler for the signal if `old` 292 | /// is non-null. 293 | unsafe fn sigaction( 294 | sig: libc::c_int, 295 | new: *const libc::sigaction, 296 | old: *mut libc::sigaction, 297 | ) -> Result<()> { 298 | if libc::sigaction(sig, new, old) == -1 { 299 | return Err(Error::last_os_error()) 300 | } 301 | 302 | Ok(()) 303 | } 304 | -------------------------------------------------------------------------------- /sudo_pair/src/template.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use std::collections::HashMap; 16 | 17 | const DEFAULT_ESCAPE_BYTE : u8 = b'%'; 18 | 19 | pub(crate) struct Spec { 20 | escape: u8, 21 | expansions: HashMap>, 22 | } 23 | 24 | impl Spec { 25 | pub(crate) fn new() -> Self { 26 | Self::default() 27 | } 28 | 29 | pub(crate) fn with_escape(escape: u8) -> Self { 30 | Self { escape, .. Self::new() } 31 | } 32 | 33 | pub(crate) fn replace>>(&mut self, literal: u8, replacement: T) { 34 | drop(self.expansions.insert(literal, replacement.into())); 35 | } 36 | 37 | pub(crate) fn expand(&self, template: &[u8]) -> Vec { 38 | // the expanded result is likely to be at least as long as the 39 | // template; if we go a little over, it's not a big deal 40 | let mut result = Vec::with_capacity(template.len()); 41 | let mut iter = template.iter().copied(); 42 | 43 | while iter.len() != 0 { 44 | // copy literally everything up to the next escape character 45 | result.extend( 46 | iter.by_ref().take_while(|b| *b != self.escape ) 47 | ); 48 | 49 | // TODO: The above take_while consumes an extra byte in 50 | // the event that it finds the escape character; this is 51 | // *mostly* okay, but we can't distinguish between the case 52 | // where a '%' was consumed and where one wasn't (for 53 | // instance at EOF). This matters because of the following 54 | // line which terminates if there's nothing left in the 55 | // template to evaluate, because the template may have ended 56 | // in a '%'! This isn't a huge deal, but it's at least worth 57 | // documenting this limitation: if your template ends in a 58 | // line '%' character, we will silently eat it. 59 | let byte = match iter.next() { 60 | Some(b) => b, 61 | None => break, 62 | }; 63 | 64 | // if the spec contains an expansion for the escaped 65 | // character, use it; otherwise, emit the character as a 66 | // literal 67 | match self.expansions.get(&byte) { 68 | Some(expansion) => result.extend_from_slice(expansion), 69 | None => result.push(byte), 70 | }; 71 | } 72 | 73 | result 74 | } 75 | } 76 | 77 | impl Default for Spec { 78 | fn default() -> Self { 79 | Self { 80 | expansions: HashMap::new(), 81 | escape: DEFAULT_ESCAPE_BYTE, 82 | } 83 | } 84 | } 85 | 86 | impl From>> for Spec { 87 | fn from(expansions: HashMap>) -> Self { 88 | Self { expansions, .. Self::new() } 89 | } 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use super::*; 95 | 96 | #[test] 97 | fn new() { 98 | let spec = Spec::new(); 99 | 100 | assert_eq!(DEFAULT_ESCAPE_BYTE, spec.escape); 101 | } 102 | 103 | #[test] 104 | fn with_escape() { 105 | let spec = Spec::with_escape(b'\\'); 106 | 107 | assert_eq!(b'\\', spec.escape); 108 | } 109 | 110 | #[test] 111 | fn from_hashmap() { 112 | let mut map = HashMap::new(); 113 | let _ = map.insert(b'x', b"abc".to_vec()); 114 | 115 | let spec = Spec::from(map.clone()); 116 | 117 | assert_eq!(map, spec.expansions); 118 | } 119 | 120 | #[test] 121 | fn no_expansions() { 122 | let spec = Spec::new(); 123 | let template = b"this has no expansions"; 124 | 125 | assert_eq!( 126 | template[..], 127 | spec.expand(template)[..] 128 | ); 129 | } 130 | 131 | #[test] 132 | fn expansions() { 133 | let mut spec = Spec::new(); 134 | let template = b"a: %a, b: %b"; 135 | 136 | let _ = spec.replace(b'a', &b"foo"[..]); 137 | let _ = spec.replace(b'b', &b"bar"[..]); 138 | 139 | assert_eq!( 140 | b"a: foo, b: bar"[..], 141 | spec.expand(template)[..], 142 | ); 143 | } 144 | 145 | #[test] 146 | fn repeated_expansions() { 147 | let mut spec = Spec::new(); 148 | let template = b"%a%a%a%b%a%a%b"; 149 | 150 | let _ = spec.replace(b'a', &b"x"[..]); 151 | let _ = spec.replace(b'b', &b"y"[..]); 152 | 153 | assert_eq!( 154 | b"xxxyxxy"[..], 155 | spec.expand(template)[..], 156 | ); 157 | } 158 | 159 | #[test] 160 | fn expansion_inserts_itself() { 161 | let mut spec = Spec::new(); 162 | let template = b"test %x test"; 163 | 164 | spec.replace(b'x', &b"x"[..]); 165 | 166 | assert_eq!( 167 | b"test x test"[..], 168 | spec.expand(template)[..], 169 | ); 170 | } 171 | 172 | #[test] 173 | fn expansion_isnt_recursive() { 174 | let mut spec = Spec::new(); 175 | let template = b"test %x test"; 176 | 177 | spec.replace(b'x', &b"%x %y %z % %%"[..]); 178 | spec.replace(b'y', &b"BUG"[..]); 179 | 180 | assert_eq!( 181 | b"test %x %y %z % %% test"[..], 182 | spec.expand(template)[..], 183 | ); 184 | } 185 | 186 | #[test] 187 | fn expansion_inserts_nothing() { 188 | let mut spec = Spec::new(); 189 | let template = b"test %X test"; 190 | 191 | spec.replace(b'X', &b""[..]); 192 | 193 | assert_eq!( 194 | b"test test"[..], 195 | spec.expand(template)[..], 196 | ); 197 | } 198 | 199 | #[test] 200 | fn unused_expansions() { 201 | let mut spec = Spec::new(); 202 | let template = b"only y should be expanded %y"; 203 | 204 | spec.replace(b'y', &b"qwerty"[..]); 205 | spec.replace(b'n', &b"uiop["[..]); 206 | 207 | assert_eq!( 208 | b"only y should be expanded qwerty"[..], 209 | spec.expand(template)[..], 210 | ); 211 | } 212 | 213 | #[test] 214 | fn literals() { 215 | let mut spec = Spec::new(); 216 | let template = b"a: %a, b: %b"; 217 | 218 | spec.replace(b'b', &b"bar"[..]); 219 | 220 | assert_eq!( 221 | b"a: a, b: bar"[..], 222 | spec.expand(template)[..], 223 | ); 224 | } 225 | 226 | #[test] 227 | fn literal_escape_character() { 228 | let spec = Spec::new(); 229 | let template = b"%%%%%%%%%%%%%%"; 230 | 231 | assert_eq!( 232 | b"%%%%%%%"[..], 233 | spec.expand(template)[..], 234 | ); 235 | } 236 | 237 | // you can currently provide an expansion for the escape character, 238 | // which prevents ever being able to insert an escape character 239 | // literal; this isn't worth fixing 240 | #[test] 241 | fn bug_wontfix_expand_escape_character() { 242 | let mut spec = Spec::new(); 243 | let template = b"|%%|"; 244 | 245 | spec.replace(b'%', &b"x"[..]); 246 | 247 | assert_eq!( 248 | b"|x|"[..], 249 | spec.expand(template)[..] 250 | ); 251 | } 252 | 253 | // `take_while` silently eats one extra character off of the `Iter`, 254 | // since it needs to call `Iter::next` before it can check the value, 255 | // and this leads us to not being able to detect the difference 256 | // between a normal EOF and an EOF immediately after a lone escape 257 | // character; this isn't worth fixing 258 | #[test] 259 | fn bug_wontfix_swallow_trailing_escape_character() { 260 | let spec = Spec::new(); 261 | let template = b"some text%"; 262 | 263 | assert_eq!( 264 | b"some text"[..], 265 | spec.expand(template)[..] 266 | ); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /sudo_plugin-sys/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | - Pre-built bindings are now committed for x86, x86-64, and aarch64 targets. 12 | - Bindings may be generated at compile-time for additional architectures with 13 | the `bindgen` feature. 14 | - Functions to operate on sudo plugin API versions, which normally exist as 15 | macros in `sudo_plugin.h` but aren't incorporated by `bindgen`. 16 | - `sudo_api_version_get_major` 17 | - `sudo_api_version_get_minor` 18 | - `sudo_api_mkversion` 19 | 20 | ### Changed 21 | - This project will now adopt the upstream sudo_plugin version number for future 22 | releases. 23 | 24 | ### Removed 25 | 26 | - Removed `sudo_printf_non_null_t`. 27 | 28 | ## [1.2.1] - 2020-03-27 29 | 30 | ### Fixed 31 | 32 | - [Fixed on 32-bit architectures][issue-59]; bindgen output cannot be 33 | committed directly, since it's architecture-dependent 34 | 35 | [issue-59]: https://github.com/square/sudo_pair/issues/59 36 | 37 | ## [1.2.0] - 2020-03-26 38 | 39 | ### Changed 40 | - Builds using Rust 2018 41 | - No longer fails to build on warnings, unless being run in CI 42 | - Bindgen-generated bindings are committed directly so we can remove 43 | bindgen from the list of build dependencies 44 | 45 | ## [1.1.0] - 2018-05-18 46 | 47 | ## Changed 48 | - Updated to use bindgen 0.37, which changes the mutability of some pointer parameters 49 | 50 | ## [1.0.1] - 2018-05-08 51 | 52 | ### Fixed 53 | - Preferentially use bundled sudo_plugin.h 54 | 55 | ## 1.0.0 - 2018-05-07 56 | 57 | ### Added 58 | - Bindings automatically generated for [sudo_plugin(8)](https://www.sudo.ws/man/1.8.22/sudo_plugin.man.html) 59 | - Provides default `sudo_plugin.h` which will be used if none is found on the system 60 | 61 | [Unreleased]: https://github.com/square/sudo_pair/compare/sudo_plugin-sys-v1.2.1...master 62 | [1.2.1]: https://github.com/square/sudo_pair/compare/sudo_plugin-sys-v1.2.0...sudo_plugin-sys-v1.2.1 63 | [1.2.0]: https://github.com/square/sudo_pair/compare/sudo_plugin-sys-v1.1.0...sudo_plugin-sys-v1.2.0 64 | [1.1.0]: https://github.com/square/sudo_pair/compare/sudo_plugin-sys-v1.0.1...sudo_plugin-sys-v1.1.0 65 | [1.0.1]: https://github.com/square/sudo_pair/compare/sudo_plugin-sys-v1.0.0...sudo_plugin-sys-v1.0.1 66 | -------------------------------------------------------------------------------- /sudo_plugin-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = 'sudo_plugin-sys' 3 | version = '1.2.1' 4 | license = 'Apache-2.0' 5 | edition = '2018' 6 | 7 | authors = ['Stephen Touset '] 8 | description = 'Bindings to the sudo plugin API' 9 | 10 | homepage = 'https://github.com/square/sudo_pair' 11 | repository = 'https://github.com/square/sudo_pair.git' 12 | readme = 'README.md' 13 | 14 | categories = ['external-ffi-bindings'] 15 | keywords = ['sudo', 'sudo-plugin', 'api-bindings'] 16 | 17 | build = 'build.rs' 18 | 19 | [build-dependencies] 20 | bindgen = { version = '0.59.0', optional = true } 21 | -------------------------------------------------------------------------------- /sudo_plugin-sys/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /sudo_plugin-sys/README.md: -------------------------------------------------------------------------------- 1 | # sudo_plugin-sys 2 | 3 | [![Build Status](https://github.com/square/sudo_pair/actions/workflows/rust.yml/badge.svg?branch=master)][badge-build] 4 | [![Docs](https://img.shields.io/docsrs/sudo_plugin-sys)][badge-docs] 5 | [![Latest Version](https://img.shields.io/crates/v/sudo_plugin-sys.svg)][badge-crate] 6 | [![License](https://img.shields.io/github/license/square/sudo_pair.svg)][license] 7 | 8 | FFI definitions for the [sudo_plugin(8)][sudo_plugin_man] facility. 9 | 10 | ## Sudo Plugin API Version 11 | 12 | This crate matches its version to the upstream [sudo_plugin(8)][sudo_plugin_man] 13 | API version it implements and will use patch versions to indicate internal fixes 14 | to the crate. 15 | 16 | Historically, the `sudo_plugin(8)` binary interface has been 17 | backwards-compatible with previous versions and respected semantic versioning 18 | practices. Since we track the upstream version numbering scheme, we believe this 19 | project can also respect semantic versioning but cannot make this a guarantee. 20 | 21 | Later versions of this plugin *should* be backward-compatible with older 22 | versions of the `sudo_plugin(8)` interface. However, the user of this API is 23 | responsible for checking the sudo front-end API version before using certain 24 | features. Please consult the [manpage][sudo_plugin_man] to identify which 25 | features require version probing. 26 | 27 | ## Building 28 | 29 | This crate includes pregenerated bindings for `x86`, `x86-64`, and `aarch64` 30 | architectures and will use them by default when building with Cargo. 31 | 32 | ```sh 33 | cargo build 34 | ``` 35 | 36 | For other architectures, you'll need to build with the `bindgen` feature 37 | enabled. This will generate bindings from the copy of `sudo_plugin.h` bundled 38 | with this library. 39 | 40 | ```sh 41 | cargo build --features bindgen 42 | ``` 43 | 44 | As releases of this library are built with a specific version of the plugin API 45 | in mind, we do not currently support building against external versions of this 46 | header. Since newer versions of the sudo plugin interface are binary-compatible 47 | with older versions (and vice versa), doing so should not be necessary. If you 48 | find a use-case that requires this, please [let us know][new-issue]. 49 | 50 | ## Usage 51 | 52 | This project simply exposes the raw sudo plugin API bindings to Rust. While 53 | these raw FFI bindings may be used directly, we encourage the use of a safe 54 | wrapper such as the [sudo_plugin][sudo_plugin] project. 55 | 56 | For a demonstration of how to use the raw FFI bindings directly, see the 57 | [raw_plugin_api][raw_plugin_api] example project. 58 | 59 | ## Contributions 60 | 61 | Contributions are welcome! 62 | 63 | As this project is operated under [Square's open source program][square-open-source], 64 | new contributors will be asked to sign a [contributor license agreement][square-cla] 65 | that ensures we can continue to develop, maintain, and release this project 66 | openly. 67 | 68 | ## License 69 | 70 | `sudo_plugin-sys` is distributed under the terms of the [Apache License, Version 71 | 2.0][license]. 72 | 73 | [badge-build]: https://github.com/square/sudo_pair/actions/workflows/rust.yml 74 | [badge-docs]: https://docs.rs/sudo_plugin-sys 75 | [badge-crate]: https://crates.io/crates/sudo_plugin-sys 76 | [license]: https://github.com/square/sudo_pair/blob/master/LICENSE-APACHE 77 | [new-issue]: https://github.com/square/sudo_pair/issues/new 78 | [raw_plugin_api]: ../examples/raw_plugin_api/ 79 | [sudo_plugin]: ../sudo_plugin/README.md 80 | [square-cla]: https://cla-assistant.io/square/sudo_pair 81 | [square-open-source]: https://square.github.io/ 82 | [sudo_plugin_man]: https://www.sudo.ws/man/sudo_plugin.man.html 83 | -------------------------------------------------------------------------------- /sudo_plugin-sys/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use std::env; 16 | use std::path::PathBuf; 17 | 18 | fn main() { 19 | let bindings_path = PathBuf::from(env::var("OUT_DIR").unwrap()) 20 | .join("bindings.rs"); 21 | 22 | bindings::generate(&bindings_path); 23 | } 24 | 25 | #[cfg(not(feature = "bindgen"))] 26 | mod bindings { 27 | use std::fs; 28 | use std::path::Path; 29 | 30 | #[cfg(target_arch = "aarch64")] 31 | const TARGET_ARCH : &str = "aarch64"; 32 | 33 | #[cfg(target_arch = "x86_64")] 34 | const TARGET_ARCH : &str = "x86-64"; 35 | 36 | #[cfg(target_arch = "x86")] 37 | const TARGET_ARCH : &str = "x86"; 38 | 39 | pub fn generate(out_path: &Path) { 40 | let in_path = format!( 41 | "src/bindings/sudo_plugin.{}.rs", 42 | TARGET_ARCH, 43 | ); 44 | 45 | fs::copy(in_path, out_path).unwrap(); 46 | } 47 | } 48 | 49 | #[cfg(feature = "bindgen")] 50 | mod bindings { 51 | use bindgen::builder; 52 | use std::path::Path; 53 | 54 | pub fn generate(out_path: &Path) { 55 | builder() 56 | .header("include/sudo_plugin.h") 57 | .generate() 58 | .unwrap() 59 | .write_to_file(out_path) 60 | .unwrap() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sudo_plugin-sys/include/sudo_plugin.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: ISC 3 | * 4 | * Copyright (c) 2009-2020 Todd C. Miller 5 | * 6 | * Permission to use, copy, modify, and distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | #ifndef SUDO_PLUGIN_H 20 | #define SUDO_PLUGIN_H 21 | 22 | /* API version major/minor */ 23 | #define SUDO_API_VERSION_MAJOR 1 24 | #define SUDO_API_VERSION_MINOR 17 25 | #define SUDO_API_MKVERSION(x, y) (((x) << 16) | (y)) 26 | #define SUDO_API_VERSION SUDO_API_MKVERSION(SUDO_API_VERSION_MAJOR, SUDO_API_VERSION_MINOR) 27 | 28 | /* Getters and setters for plugin API versions */ 29 | #define SUDO_API_VERSION_GET_MAJOR(v) ((v) >> 16) 30 | #define SUDO_API_VERSION_GET_MINOR(v) ((v) & 0xffffU) 31 | #define SUDO_API_VERSION_SET_MAJOR(vp, n) do { \ 32 | *(vp) = (*(vp) & 0x0000ffffU) | ((n) << 16); \ 33 | } while(0) 34 | #define SUDO_API_VERSION_SET_MINOR(vp, n) do { \ 35 | *(vp) = (*(vp) & 0xffff0000U) | (n); \ 36 | } while(0) 37 | 38 | /* "plugin type" for the sudo front end, as passed to an audit plugin */ 39 | #define SUDO_FRONT_END 0 40 | 41 | /* Conversation function types and defines */ 42 | struct sudo_conv_message { 43 | #define SUDO_CONV_PROMPT_ECHO_OFF 0x0001 /* do not echo user input */ 44 | #define SUDO_CONV_PROMPT_ECHO_ON 0x0002 /* echo user input */ 45 | #define SUDO_CONV_ERROR_MSG 0x0003 /* error message */ 46 | #define SUDO_CONV_INFO_MSG 0x0004 /* informational message */ 47 | #define SUDO_CONV_PROMPT_MASK 0x0005 /* mask user input */ 48 | #define SUDO_CONV_PROMPT_ECHO_OK 0x1000 /* flag: allow echo if no tty */ 49 | #define SUDO_CONV_PREFER_TTY 0x2000 /* flag: use tty if possible */ 50 | int msg_type; 51 | int timeout; 52 | const char *msg; 53 | }; 54 | 55 | /* 56 | * Maximum length of a reply (not including the trailing NUL) when 57 | * conversing with the user. In practical terms, this is the longest 58 | * password sudo will support. This means that a buffer of size 59 | * SUDO_CONV_REPL_MAX+1 is guaranteed to be able to hold any reply 60 | * from the conversation function. 61 | */ 62 | #define SUDO_CONV_REPL_MAX 1023 63 | 64 | struct sudo_conv_reply { 65 | char *reply; 66 | }; 67 | 68 | /* Conversation callback API version major/minor */ 69 | #define SUDO_CONV_CALLBACK_VERSION_MAJOR 1 70 | #define SUDO_CONV_CALLBACK_VERSION_MINOR 0 71 | #define SUDO_CONV_CALLBACK_VERSION SUDO_API_MKVERSION(SUDO_CONV_CALLBACK_VERSION_MAJOR, SUDO_CONV_CALLBACK_VERSION_MINOR) 72 | 73 | /* 74 | * Callback struct to be passed to the conversation function. 75 | * Can be used to perform operations on suspend/resume such 76 | * as dropping/acquiring locks. 77 | */ 78 | typedef int (*sudo_conv_callback_fn_t)(int signo, void *closure); 79 | struct sudo_conv_callback { 80 | unsigned int version; 81 | void *closure; 82 | sudo_conv_callback_fn_t on_suspend; 83 | sudo_conv_callback_fn_t on_resume; 84 | }; 85 | 86 | typedef int (*sudo_conv_t)(int num_msgs, const struct sudo_conv_message msgs[], 87 | struct sudo_conv_reply replies[], struct sudo_conv_callback *callback); 88 | typedef int (*sudo_printf_t)(int msg_type, const char *fmt, ...); 89 | 90 | /* 91 | * Hooks allow a plugin to hook into specific sudo and/or libc functions. 92 | */ 93 | 94 | #if defined(__GNUC__) && ((__GNUC__ == 4 && __GNUC_MINOR__ >= 4) || __GNUC__ > 4) 95 | # pragma GCC diagnostic ignored "-Wstrict-prototypes" 96 | #endif 97 | 98 | /* Hook functions typedefs. */ 99 | typedef int (*sudo_hook_fn_t)(); 100 | typedef int (*sudo_hook_fn_setenv_t)(const char *name, const char *value, int overwrite, void *closure); 101 | typedef int (*sudo_hook_fn_putenv_t)(char *string, void *closure); 102 | typedef int (*sudo_hook_fn_getenv_t)(const char *name, char **value, void *closure); 103 | typedef int (*sudo_hook_fn_unsetenv_t)(const char *name, void *closure); 104 | 105 | /* Hook structure definition. */ 106 | struct sudo_hook { 107 | unsigned int hook_version; 108 | unsigned int hook_type; 109 | sudo_hook_fn_t hook_fn; 110 | void *closure; 111 | }; 112 | 113 | /* Hook API version major/minor */ 114 | #define SUDO_HOOK_VERSION_MAJOR 1 115 | #define SUDO_HOOK_VERSION_MINOR 0 116 | #define SUDO_HOOK_VERSION SUDO_API_MKVERSION(SUDO_HOOK_VERSION_MAJOR, SUDO_HOOK_VERSION_MINOR) 117 | 118 | /* 119 | * Hook function return values. 120 | */ 121 | #define SUDO_HOOK_RET_ERROR -1 /* error */ 122 | #define SUDO_HOOK_RET_NEXT 0 /* go to the next hook in the list */ 123 | #define SUDO_HOOK_RET_STOP 1 /* stop hook processing for this type */ 124 | 125 | /* 126 | * Hooks for setenv/unsetenv/putenv/getenv. 127 | * This allows the plugin to be notified when a PAM module modifies 128 | * the environment so it can update the copy of the environment that 129 | * is passed to execve(). 130 | */ 131 | #define SUDO_HOOK_SETENV 1 132 | #define SUDO_HOOK_UNSETENV 2 133 | #define SUDO_HOOK_PUTENV 3 134 | #define SUDO_HOOK_GETENV 4 135 | 136 | /* 137 | * Plugin interface to sudo's main event loop. 138 | */ 139 | typedef void (*sudo_plugin_ev_callback_t)(int fd, int what, void *closure); 140 | 141 | struct timespec; 142 | struct sudo_plugin_event { 143 | int (*set)(struct sudo_plugin_event *pev, int fd, int events, sudo_plugin_ev_callback_t callback, void *closure); 144 | int (*add)(struct sudo_plugin_event *pev, struct timespec *timeout); 145 | int (*del)(struct sudo_plugin_event *pev); 146 | int (*pending)(struct sudo_plugin_event *pev, int events, struct timespec *ts); 147 | int (*fd)(struct sudo_plugin_event *pev); 148 | void (*setbase)(struct sudo_plugin_event *pev, void *base); 149 | void (*loopbreak)(struct sudo_plugin_event *pev); 150 | void (*free)(struct sudo_plugin_event *pev); 151 | /* actually larger... */ 152 | }; 153 | 154 | /* Sudo plugin Event types */ 155 | #define SUDO_PLUGIN_EV_TIMEOUT 0x01 /* fire after timeout */ 156 | #define SUDO_PLUGIN_EV_READ 0x02 /* fire when readable */ 157 | #define SUDO_PLUGIN_EV_WRITE 0x04 /* fire when writable */ 158 | #define SUDO_PLUGIN_EV_PERSIST 0x08 /* persist until deleted */ 159 | #define SUDO_PLUGIN_EV_SIGNAL 0x10 /* fire on signal receipt */ 160 | 161 | /* Policy plugin type and defines. */ 162 | struct passwd; 163 | struct policy_plugin { 164 | #define SUDO_POLICY_PLUGIN 1 165 | unsigned int type; /* always SUDO_POLICY_PLUGIN */ 166 | unsigned int version; /* always SUDO_API_VERSION */ 167 | int (*open)(unsigned int version, sudo_conv_t conversation, 168 | sudo_printf_t sudo_printf, char * const settings[], 169 | char * const user_info[], char * const user_env[], 170 | char * const plugin_options[], const char **errstr); 171 | void (*close)(int exit_status, int error); /* wait status or error */ 172 | int (*show_version)(int verbose); 173 | int (*check_policy)(int argc, char * const argv[], 174 | char *env_add[], char **command_info[], 175 | char **argv_out[], char **user_env_out[], const char **errstr); 176 | int (*list)(int argc, char * const argv[], int verbose, 177 | const char *list_user, const char **errstr); 178 | int (*validate)(const char **errstr); 179 | void (*invalidate)(int remove); 180 | int (*init_session)(struct passwd *pwd, char **user_env_out[], 181 | const char **errstr); 182 | void (*register_hooks)(int version, int (*register_hook)(struct sudo_hook *hook)); 183 | void (*deregister_hooks)(int version, int (*deregister_hook)(struct sudo_hook *hook)); 184 | struct sudo_plugin_event * (*event_alloc)(void); 185 | }; 186 | 187 | /* I/O plugin type and defines. */ 188 | struct io_plugin { 189 | #define SUDO_IO_PLUGIN 2 190 | unsigned int type; /* always SUDO_IO_PLUGIN */ 191 | unsigned int version; /* always SUDO_API_VERSION */ 192 | int (*open)(unsigned int version, sudo_conv_t conversation, 193 | sudo_printf_t sudo_printf, char * const settings[], 194 | char * const user_info[], char * const command_info[], 195 | int argc, char * const argv[], char * const user_env[], 196 | char * const plugin_options[], const char **errstr); 197 | void (*close)(int exit_status, int error); /* wait status or error */ 198 | int (*show_version)(int verbose); 199 | int (*log_ttyin)(const char *buf, unsigned int len, const char **errstr); 200 | int (*log_ttyout)(const char *buf, unsigned int len, const char **errstr); 201 | int (*log_stdin)(const char *buf, unsigned int len, const char **errstr); 202 | int (*log_stdout)(const char *buf, unsigned int len, const char **errstr); 203 | int (*log_stderr)(const char *buf, unsigned int len, const char **errstr); 204 | void (*register_hooks)(int version, 205 | int (*register_hook)(struct sudo_hook *hook)); 206 | void (*deregister_hooks)(int version, 207 | int (*deregister_hook)(struct sudo_hook *hook)); 208 | int (*change_winsize)(unsigned int line, unsigned int cols, 209 | const char **errstr); 210 | int (*log_suspend)(int signo, const char **errstr); 211 | struct sudo_plugin_event * (*event_alloc)(void); 212 | }; 213 | 214 | /* Differ audit plugin close status types. */ 215 | #define SUDO_PLUGIN_NO_STATUS 0 216 | #define SUDO_PLUGIN_WAIT_STATUS 1 217 | #define SUDO_PLUGIN_EXEC_ERROR 2 218 | #define SUDO_PLUGIN_SUDO_ERROR 3 219 | 220 | /* Audit plugin type and defines */ 221 | struct audit_plugin { 222 | #define SUDO_AUDIT_PLUGIN 3 223 | unsigned int type; /* always SUDO_AUDIT_PLUGIN */ 224 | unsigned int version; /* always SUDO_API_VERSION */ 225 | int (*open)(unsigned int version, sudo_conv_t conversation, 226 | sudo_printf_t sudo_printf, char * const settings[], 227 | char * const user_info[], int submit_optind, 228 | char * const submit_argv[], char * const submit_envp[], 229 | char * const plugin_options[], const char **errstr); 230 | void (*close)(int status_type, int status); 231 | int (*accept)(const char *plugin_name, unsigned int plugin_type, 232 | char * const command_info[], char * const run_argv[], 233 | char * const run_envp[], const char **errstr); 234 | int (*reject)(const char *plugin_name, unsigned int plugin_type, 235 | const char *audit_msg, char * const command_info[], 236 | const char **errstr); 237 | int (*error)(const char *plugin_name, unsigned int plugin_type, 238 | const char *audit_msg, char * const command_info[], 239 | const char **errstr); 240 | int (*show_version)(int verbose); 241 | void (*register_hooks)(int version, int (*register_hook)(struct sudo_hook *hook)); 242 | void (*deregister_hooks)(int version, int (*deregister_hook)(struct sudo_hook *hook)); 243 | struct sudo_plugin_event * (*event_alloc)(void); 244 | }; 245 | 246 | /* Approval plugin type and defines */ 247 | struct approval_plugin { 248 | #define SUDO_APPROVAL_PLUGIN 4 249 | unsigned int type; /* always SUDO_APPROVAL_PLUGIN */ 250 | unsigned int version; /* always SUDO_API_VERSION */ 251 | int (*open)(unsigned int version, sudo_conv_t conversation, 252 | sudo_printf_t sudo_printf, char * const settings[], 253 | char * const user_info[], int submit_optind, 254 | char * const submit_argv[], char * const submit_envp[], 255 | char * const plugin_options[], const char **errstr); 256 | void (*close)(void); 257 | int (*check)(char * const command_info[], char * const run_argv[], 258 | char * const run_envp[], const char **errstr); 259 | int (*show_version)(int verbose); 260 | }; 261 | 262 | /* Sudoers group plugin version major/minor */ 263 | #define GROUP_API_VERSION_MAJOR 1 264 | #define GROUP_API_VERSION_MINOR 0 265 | #define GROUP_API_VERSION SUDO_API_MKVERSION(GROUP_API_VERSION_MAJOR, GROUP_API_VERSION_MINOR) 266 | 267 | /* Getters and setters for group version (for source compat only) */ 268 | #define GROUP_API_VERSION_GET_MAJOR(v) SUDO_API_VERSION_GET_MAJOR(v) 269 | #define GROUP_API_VERSION_GET_MINOR(v) SUDO_API_VERSION_GET_MINOR(v) 270 | #define GROUP_API_VERSION_SET_MAJOR(vp, n) SUDO_API_VERSION_SET_MAJOR(vp, n) 271 | #define GROUP_API_VERSION_SET_MINOR(vp, n) SUDO_API_VERSION_SET_MINOR(vp, n) 272 | 273 | /* 274 | * version: for compatibility checking 275 | * group_init: return 1 on success, 0 if unconfigured, -1 on error. 276 | * group_cleanup: called to clean up resources used by provider 277 | * user_in_group: returns 1 if user is in group, 0 if not. 278 | * note that pwd may be NULL if the user is not in passwd. 279 | */ 280 | struct sudoers_group_plugin { 281 | unsigned int version; 282 | int (*init)(int version, sudo_printf_t sudo_printf, char *const argv[]); 283 | void (*cleanup)(void); 284 | int (*query)(const char *user, const char *group, const struct passwd *pwd); 285 | }; 286 | 287 | #endif /* SUDO_PLUGIN_H */ 288 | -------------------------------------------------------------------------------- /sudo_plugin-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | //! This crate is a (lighly enhanced) set of bindgen-generated Rust FFI 16 | //! bindings for the [`sudo_plugin(8)`][sudo_plugin] facility. In 17 | //! general, it is expected that end-users will prefer to use the 18 | //! handcrafted Rust wrappers from the `sudo_plugin` crate which 19 | //! accompanies this project. 20 | //! 21 | //! # Example: 22 | //! 23 | //! ```rust 24 | //! use sudo_plugin_sys as sudo; 25 | //! use std::os::raw; 26 | //! 27 | //! #[no_mangle] 28 | //! pub static mut deny_everything: sudo::approval_plugin = sudo::approval_plugin { 29 | //! open: Some(open), 30 | //! check: Some(check), 31 | //! show_version: Some(show_version), 32 | //! 33 | //! .. sudo::approval_plugin::empty() 34 | //! }; 35 | //! 36 | //! static mut SUDO_CONV: sudo::sudo_conv_t = None; 37 | //! static mut SUDO_PRINT: sudo::sudo_printf_t = None; 38 | //! static mut SUDO_API_VERSION: Option = None; 39 | //! 40 | //! const VERSION_MSG: *const raw::c_char = b"Deny Everything version 1.0\n\0".as_ptr().cast(); 41 | //! const ERRSTR: *const raw::c_char = b"deny_everything: denied\n\0" .as_ptr().cast(); 42 | //! 43 | //! unsafe extern "C" fn open( 44 | //! version: raw::c_uint, 45 | //! conversation: sudo::sudo_conv_t, 46 | //! sudo_printf: sudo::sudo_printf_t, 47 | //! _settings: *const *mut raw::c_char, 48 | //! _user_info: *const *mut raw::c_char, 49 | //! _submit_optind: raw::c_int, 50 | //! _submit_argv: *const *mut raw::c_char, 51 | //! _submit_envp: *const *mut raw::c_char, 52 | //! _plugin_options: *const *mut raw::c_char, 53 | //! _errstr: *mut *const raw::c_char, 54 | //! ) -> raw::c_int { 55 | //! SUDO_CONV = conversation; 56 | //! SUDO_PRINT = sudo_printf; 57 | //! SUDO_API_VERSION = Some(version); 58 | //! 59 | //! 1 60 | //! } 61 | //! 62 | //! unsafe extern "C" fn check( 63 | //! _command_info: *const *mut raw::c_char, 64 | //! _run_argv: *const *mut raw::c_char, 65 | //! _run_envp: *const *mut raw::c_char, 66 | //! errstr: *mut *const raw::c_char, 67 | //! ) -> raw::c_int { 68 | //! let print = match SUDO_PRINT { 69 | //! Some(f) => f, 70 | //! None => { return 0 }, 71 | //! }; 72 | //! 73 | //! #[allow(clippy::cast_possible_wrap)] 74 | //! let _ = (print)(sudo::SUDO_CONV_INFO_MSG as _, ERRSTR); 75 | //! 76 | //! if SUDO_API_VERSION.map(|v| v >= sudo::sudo_api_mkversion(1, 17)) == Some(true) { 77 | //! *errstr = ERRSTR; 78 | //! } 79 | //! 80 | //! 0 81 | //! } 82 | //! 83 | //! unsafe extern "C" fn show_version(_verbose: raw::c_int) -> raw::c_int { 84 | //! let print = match SUDO_PRINT { 85 | //! Some(f) => f, 86 | //! None => { return 0 }, 87 | //! }; 88 | //! 89 | //! #[allow(clippy::cast_possible_wrap)] 90 | //! let _ = (print)(sudo::SUDO_CONV_INFO_MSG as _, VERSION_MSG); 91 | //! 92 | //! 1 93 | //! } 94 | //! ``` 95 | //! 96 | //! [sudo_plugin]: https://www.sudo.ws/man/1.8.22/sudo_plugin.man.html 97 | 98 | #![warn(future_incompatible)] 99 | #![warn(nonstandard_style)] 100 | #![warn(rust_2021_compatibility)] 101 | #![warn(rust_2018_compatibility)] 102 | #![warn(rust_2018_idioms)] 103 | #![warn(unused)] 104 | 105 | #![warn(bare_trait_objects)] 106 | #![warn(missing_copy_implementations)] 107 | #![warn(missing_debug_implementations)] 108 | #![warn(missing_docs)] 109 | #![warn(single_use_lifetimes)] 110 | #![warn(unreachable_pub)] 111 | #![warn(unstable_features)] 112 | #![warn(unused_import_braces)] 113 | #![warn(unused_lifetimes)] 114 | #![warn(unused_qualifications)] 115 | #![warn(unused_results)] 116 | #![warn(variant_size_differences)] 117 | 118 | #![warn(rustdoc::all)] 119 | 120 | #![warn(clippy::cargo)] 121 | #![warn(clippy::complexity)] 122 | #![warn(clippy::correctness)] 123 | #![warn(clippy::pedantic)] 124 | #![warn(clippy::perf)] 125 | #![warn(clippy::style)] 126 | 127 | mod sys { 128 | // this entire module is unsafe code 129 | #![allow(unsafe_code)] 130 | 131 | // this entire module is generated code 132 | #![allow(missing_docs)] 133 | #![allow(non_camel_case_types)] 134 | 135 | // this entire module is generated code 136 | #![allow(clippy::similar_names)] 137 | #![allow(clippy::type_complexity)] 138 | 139 | // FIXME: https://github.com/rust-lang/rust-bindgen/issues/1651 140 | #![allow(deref_nullptr)] 141 | 142 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 143 | } 144 | 145 | pub use sys::*; 146 | 147 | use std::os::raw::c_uint; 148 | 149 | /// Constructs a sudo API verson from a major version and a minor version. 150 | /// 151 | /// # Example: 152 | /// 153 | /// ```rust 154 | /// use sudo_plugin_sys as sudo; 155 | /// 156 | /// let v1_9 = sudo::sudo_api_mkversion(1, 9); 157 | /// let v1_0 = sudo::sudo_api_mkversion(1, 0); 158 | /// let v1_17 = sudo::sudo_api_mkversion(1, 17); 159 | /// 160 | /// assert!(v1_0 < v1_9); 161 | /// assert!(v1_0 < v1_17); 162 | /// assert!(v1_9 < v1_17); 163 | /// ``` 164 | #[must_use] 165 | pub const fn sudo_api_mkversion(major: c_uint, minor: c_uint) -> c_uint { 166 | major << 16 | minor 167 | } 168 | 169 | /// Gets the major version component from a sudo API version. 170 | /// 171 | /// # Example: 172 | /// 173 | /// ```rust 174 | /// use sudo_plugin_sys as sudo; 175 | /// 176 | /// let v1_0 = sudo::sudo_api_mkversion(1, 0); 177 | /// let v1_17 = sudo::sudo_api_mkversion(1, 17); 178 | /// let v99_1 = sudo::sudo_api_mkversion(99, 1); 179 | /// 180 | /// assert_eq!(1, sudo::sudo_api_version_get_major(v1_0)); 181 | /// assert_eq!(1, sudo::sudo_api_version_get_major(v1_17)); 182 | /// assert_eq!(99, sudo::sudo_api_version_get_major(v99_1)); 183 | /// ``` 184 | #[must_use] 185 | pub const fn sudo_api_version_get_major(version: c_uint) -> c_uint { 186 | version >> 16 187 | } 188 | 189 | /// Gets the minor version component from a sudo API version. 190 | /// 191 | /// # Example: 192 | /// 193 | /// ```rust 194 | /// use sudo_plugin_sys as sudo; 195 | /// 196 | /// let v1_0 = sudo::sudo_api_mkversion(1, 0); 197 | /// let v1_17 = sudo::sudo_api_mkversion(1, 17); 198 | /// let v99_1 = sudo::sudo_api_mkversion(99, 1); 199 | /// 200 | /// assert_eq!(0, sudo::sudo_api_version_get_minor(v1_0)); 201 | /// assert_eq!(17, sudo::sudo_api_version_get_minor(v1_17)); 202 | /// assert_eq!(1, sudo::sudo_api_version_get_minor(v99_1)); 203 | /// ``` 204 | #[must_use] 205 | pub const fn sudo_api_version_get_minor(version: c_uint) -> c_uint { 206 | version & 0xffff 207 | } 208 | 209 | /// The version of the sudo API this extension supports. 210 | pub const SUDO_API_VERSION: c_uint = sudo_api_mkversion( 211 | SUDO_API_VERSION_MAJOR, 212 | SUDO_API_VERSION_MINOR, 213 | ); 214 | 215 | impl policy_plugin { 216 | const EMPTY : Self = Self { 217 | type_: SUDO_POLICY_PLUGIN, 218 | version: SUDO_API_VERSION, 219 | 220 | open: None, 221 | close: None, 222 | show_version: None, 223 | check_policy: None, 224 | list: None, 225 | validate: None, 226 | invalidate: None, 227 | init_session: None, 228 | register_hooks: None, 229 | deregister_hooks: None, 230 | event_alloc: None, 231 | }; 232 | 233 | /// Returns an empty instance of this plugin that provides no 234 | /// implementations for any callback. 235 | #[must_use] 236 | pub const fn empty() -> Self { Self::EMPTY } 237 | } 238 | 239 | impl io_plugin { 240 | const EMPTY : Self = Self { 241 | type_: SUDO_IO_PLUGIN, 242 | version: SUDO_API_VERSION, 243 | 244 | open: None, 245 | close: None, 246 | show_version: None, 247 | log_ttyin: None, 248 | log_ttyout: None, 249 | log_stdin: None, 250 | log_stdout: None, 251 | log_stderr: None, 252 | register_hooks: None, 253 | deregister_hooks: None, 254 | change_winsize: None, 255 | log_suspend: None, 256 | event_alloc: None, 257 | }; 258 | 259 | /// Returns an empty instance of this plugin that provides no 260 | /// implementations for any callback. 261 | #[must_use] 262 | pub const fn empty() -> Self { Self::EMPTY } 263 | } 264 | 265 | impl audit_plugin { 266 | const EMPTY : Self = Self { 267 | type_: SUDO_AUDIT_PLUGIN, 268 | version: SUDO_API_VERSION, 269 | 270 | open: None, 271 | close: None, 272 | accept: None, 273 | reject: None, 274 | error: None, 275 | show_version: None, 276 | register_hooks: None, 277 | deregister_hooks: None, 278 | event_alloc: None, 279 | }; 280 | 281 | /// Returns an empty instance of this plugin that provides no 282 | /// implementations for any callback. 283 | #[must_use] 284 | pub const fn empty() -> Self { Self::EMPTY } 285 | } 286 | 287 | impl approval_plugin { 288 | const EMPTY: Self = Self { 289 | type_: SUDO_APPROVAL_PLUGIN, 290 | version: SUDO_API_VERSION, 291 | 292 | open: None, 293 | close: None, 294 | check: None, 295 | show_version: None, 296 | }; 297 | 298 | /// Returns an empty instance of this plugin that provides no 299 | /// implementations for any callback. 300 | #[must_use] 301 | pub const fn empty() -> Self { Self::EMPTY } 302 | } 303 | -------------------------------------------------------------------------------- /sudo_plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | - Incorporated `plugin_name` and `plugin_version` into the `Plugin` struct 12 | - `Drop::drop` is called on plugins when sudo exits 13 | - Support for the `change_winsize` callback. Requires sudo 1.8.21 or greater. 14 | 15 | ### Changed 16 | - Wrapped `printf_facility` plugin argument into a dedicated `PrintFacility` 17 | struct to wrap all user communication. 18 | - Moved much of the work done in the `sudo_io_plugin!` macro to non-macro code. 19 | - Almost complete rewrite of the internals to be more Rust-like, and use traits 20 | to reduce the amount of macro magic necessary. 21 | - Autodetect what version of the `sudo_plugin` API is supported by the version 22 | of `sudo_plugin-sys` we're linked against. 23 | - Now uses a prelude-style import. 24 | - Replaced error-chain with thiserror. 25 | - Completely reworked error handling. Plugin implementations may now provide 26 | coercions from sudo_plugin internal errors to appropriate return codes. The 27 | provided `Error` type defaults to aborting the session on error. 28 | - Plugins no longer mutable when calling `log_*` functions. 29 | 30 | ### Fixed 31 | - Panics inside of plugins no longer cause undefined behavior by 32 | crossing FFI boundaries. All panics are caught at the boundaries and 33 | turned into an appropriate error type. 34 | 35 | - No longer segfaults against sudo >= 1.9 which introduced API changes that 36 | require the ability to write into the plugin. 37 | 38 | ## [1.2.0] - 2020-03-26 39 | 40 | ### Added 41 | - Automatic support for `-V` flag, which prints the version of `sudo` and any 42 | active plugins 43 | 44 | ### Changed 45 | - Builds using Rust 2018 46 | - No longer fails to build on warnings, unless being run in CI 47 | - Allows plugins to use any error library they wish, as long as the error 48 | types returned in `Result`s implement `Into`. 49 | 50 | ## [1.1.0] - 2018-05-18 51 | 52 | ### Added 53 | - Support writing directly to the user's TTY 54 | 55 | ### Changed 56 | - `UserInfo::tty` is now a `PathBuf` instead of a `String`. 57 | - Depends on `sudo_plugin-sys` ~1.1, which changed mutability of pointer arguments due to bindgen 0.37 58 | 59 | ## 1.0.0 - 2018-05-07 60 | 61 | ### Added 62 | - Macros to simplify writing sudo plugins 63 | - Full compatibility with plugin API versions up to 1.12 64 | 65 | [Unreleased]: https://github.com/square/sudo_pair/compare/sudo_pair-v1.2.0...master 66 | [1.2.0]: https://github.com/square/sudo_pair/compare/sudo_pair-v1.1.0...sudo_pair-v1.2.0 67 | [1.1.0]: https://github.com/square/sudo_pair/compare/sudo_pair-v1.0.0...sudo_pair-v1.1.0 68 | -------------------------------------------------------------------------------- /sudo_plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = 'sudo_plugin' 3 | version = '1.2.0' 4 | license = 'Apache-2.0' 5 | edition = '2018' 6 | 7 | authors = ['Stephen Touset '] 8 | description = 'Macros to easily write custom sudo plugins' 9 | 10 | homepage = 'https://github.com/square/sudo_pair' 11 | repository = 'https://github.com/square/sudo_pair.git' 12 | readme = '../README.md' 13 | 14 | categories = [ 'external-ffi-bindings' ] 15 | keywords = [ 'sudo', 'sudo-plugin' ] 16 | 17 | [dependencies] 18 | libc = '0.2.70' 19 | thiserror = '1.0' 20 | 21 | [dependencies.sudo_plugin-sys] 22 | version = '1.2' 23 | path = '../sudo_plugin-sys' 24 | -------------------------------------------------------------------------------- /sudo_plugin/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /sudo_plugin/src/core.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | //! This module implements the actual `sudo_plugin(8)` callbacks that 16 | //! convert between C and Rust style and trampoline into the acutal 17 | //! plugin code. It is not intended for direct end-user use. 18 | 19 | // This is entirely called from `C` code. Rust `#[must_use]` attributes 20 | // aren't going to affect anything. 21 | #![allow(clippy::must_use_candidate)] 22 | 23 | use crate::errors::{Error, SudoError}; 24 | use crate::output::PrintFacility; 25 | use crate::plugin::{IoEnv, IoPlugin, IoState}; 26 | use crate::sys; 27 | 28 | use std::os::raw; 29 | use std::path::PathBuf; 30 | use std::panic::{catch_unwind, UnwindSafe}; 31 | 32 | /// Return codes understood by the `io_plugin.open` callback. 33 | /// 34 | /// The interpretations of these values are badly-documented within the 35 | /// [`sudo_plugin(8)` manpage][manpage] so the code was used to 36 | /// understand their actual effects. 37 | /// 38 | /// [manpage]: https://www.sudo.ws/man/1.8.30/sudo_plugin.man.html 39 | /// [code]: https://github.com/sudo-project/sudo/blob/446ae3f507271c8a08f054c9291cb8804afe81d9/src/sudo.c#L1404 40 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 41 | #[repr(i32)] 42 | pub enum OpenStatus { 43 | /// The plugin was `open`ed successfully and may be used as normal. 44 | Ok = 1, 45 | 46 | /// The plugin should be unloaded for the duration of this `sudo` 47 | /// session. The `sudo` session may continue, but will not use any 48 | /// of the features of this plugin. 49 | Disable = 0, 50 | 51 | /// The `sudo` command is unauthorized and must be immediately 52 | /// terminated. 53 | Deny = -1, 54 | 55 | /// The `sudo` command was invoked incorrectly and will be 56 | /// terminated. Basic usage information will be presented to the 57 | /// user. The plugin may choose to emit its own usage information 58 | /// describing the problem. 59 | Usage = -2, 60 | } 61 | 62 | /// Return codes understood by the `io_plugin.log_*` family of callbacks. 63 | /// 64 | /// The interpretations of these values are badly-documented within the 65 | /// [`sudo_plugin(8)` manpage][manpage] so the code was used to 66 | /// understand their actual effects. 67 | /// 68 | /// [manpage]: https://www.sudo.ws/man/1.8.30/sudo_plugin.man.html 69 | /// [code]: https://github.com/sudo-project/sudo/blob/446ae3f507271c8a08f054c9291cb8804afe81d9/src/sudo.c#L1404 70 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 71 | #[repr(i32)] 72 | pub enum LogStatus { 73 | /// The plugin logged the information successfully. 74 | Ok = 1, 75 | 76 | /// The plugin has determined that the `sudo` session should be 77 | /// terminated immediately. 78 | Deny = 0, 79 | 80 | /// The plugin no longer needs this callback. This callback will no 81 | /// longer be invoked by `sudo`, but the rest of the plugin's 82 | /// callbacks will function as normal. 83 | Disable = -1, 84 | } 85 | 86 | impl From> for OpenStatus { 87 | fn from(result: Result) -> Self { 88 | match result { 89 | Ok(_) => OpenStatus::Ok, 90 | Err(e) => e.into(), 91 | } 92 | } 93 | } 94 | 95 | impl From> for LogStatus { 96 | fn from(result: Result) -> Self { 97 | match result { 98 | Ok(_) => LogStatus::Ok, 99 | Err(e) => e.into(), 100 | } 101 | } 102 | } 103 | 104 | impl> From> for OpenStatus { 105 | fn from(result: std::thread::Result) -> Self { 106 | match result { 107 | Ok(v) => v.into(), 108 | Err(_) => Error::UncaughtPanic.into(), 109 | } 110 | } 111 | } 112 | 113 | impl> From> for LogStatus { 114 | fn from(result: std::thread::Result) -> Self { 115 | match result { 116 | Ok(v) => v.into(), 117 | Err(_) => Error::UncaughtPanic.into(), 118 | } 119 | } 120 | } 121 | 122 | fn catch_unwind_open, F: FnOnce() -> T + UnwindSafe>(f: F) -> i32 { 123 | Into::::into(catch_unwind(f)) as _ 124 | } 125 | 126 | fn catch_unwind_log, F: FnOnce() -> T + UnwindSafe>(f: F) -> i32 { 127 | Into::::into(catch_unwind(f)) as _ 128 | } 129 | 130 | #[doc(hidden)] 131 | pub unsafe extern "C" fn open>( 132 | version: raw::c_uint, 133 | conversation: sys::sudo_conv_t, 134 | plugin_printf: sys::sudo_printf_t, 135 | settings_ptr: *const *mut raw::c_char, 136 | user_info_ptr: *const *mut raw::c_char, 137 | command_info_ptr: *const *mut raw::c_char, 138 | argc: raw::c_int, 139 | argv: *const *mut raw::c_char, 140 | user_env_ptr: *const *mut raw::c_char, 141 | plugin_options_ptr: *const *mut raw::c_char, 142 | _errstr: *mut *const raw::c_char, 143 | ) -> raw::c_int { 144 | catch_unwind_open(|| { 145 | // create our own PrintFacility to log to in case IoEnv 146 | // initialization fails 147 | let (_, mut stderr) = PrintFacility::new( 148 | Some(P::NAME), plugin_printf 149 | ); 150 | 151 | let io_env = IoEnv::new( 152 | P::NAME, 153 | P::VERSION, 154 | version, 155 | argc, argv, 156 | settings_ptr, 157 | user_info_ptr, 158 | command_info_ptr, 159 | user_env_ptr, 160 | plugin_options_ptr, 161 | plugin_printf, 162 | conversation, 163 | ); 164 | 165 | let io_env = match io_env { 166 | Ok(v) => v, 167 | Err(e) => { 168 | let _ = stderr.write_error(&e); 169 | let e : P::Error = e.into(); 170 | return Into::::into(e); 171 | } 172 | }; 173 | 174 | S::init(io_env, |env| { 175 | // even though we're avoiding instantiating the plugin 176 | // fully, we need to make sure it makes its way into static 177 | // storage before returning, which is why we put this check 178 | // inside `S::init` 179 | if env.command_info.command == PathBuf::default() { 180 | return Err(OpenStatus::Ok); 181 | } 182 | 183 | P::open(env).map_err(|e| { 184 | let _ = stderr.write_error(&e); 185 | Into::::into(e) 186 | }) 187 | }) 188 | }) 189 | } 190 | 191 | #[doc(hidden)] 192 | pub unsafe extern "C" fn close>( 193 | exit_status: raw::c_int, 194 | error: raw::c_int, 195 | ) { 196 | drop(catch_unwind(|| { 197 | S::drop(|plugin| plugin.close(exit_status, error)); 198 | })); 199 | } 200 | 201 | #[doc(hidden)] 202 | pub unsafe extern "C" fn show_version>( 203 | verbose: raw::c_int, 204 | ) -> raw::c_int { 205 | catch_unwind_open(|| { 206 | P::show_version(S::io_env(), verbose != 0); 207 | 208 | OpenStatus::Ok 209 | }) 210 | } 211 | 212 | #[doc(hidden)] 213 | pub unsafe extern "C" fn log_ttyin>( 214 | buf: *const raw::c_char, 215 | len: raw::c_uint, 216 | _errstr: *mut *const raw::c_char, 217 | ) -> raw::c_int { 218 | catch_unwind_log(|| { 219 | let env = S::io_env(); 220 | let plugin = S::io_plugin(); 221 | 222 | if !env.command_info.iolog_ttyin && !P::IGNORE_IOLOG_HINTS { 223 | return Ok(()); 224 | } 225 | 226 | let slice = ::std::slice::from_raw_parts( 227 | buf.cast(), 228 | len as _, 229 | ); 230 | 231 | plugin.log_ttyin(slice).map_err(|err| { 232 | let _ = env.stderr().write_error(&err); 233 | err 234 | }) 235 | }) 236 | } 237 | 238 | #[doc(hidden)] 239 | pub unsafe extern "C" fn log_ttyout>( 240 | buf: *const raw::c_char, 241 | len: raw::c_uint, 242 | _errstr: *mut *const raw::c_char, 243 | ) -> raw::c_int { 244 | catch_unwind_log(|| { 245 | let env = S::io_env(); 246 | let plugin = S::io_plugin(); 247 | 248 | if !env.command_info.iolog_ttyout && !P::IGNORE_IOLOG_HINTS { 249 | return Ok(()); 250 | } 251 | 252 | let slice = ::std::slice::from_raw_parts( 253 | buf.cast(), 254 | len as _, 255 | ); 256 | 257 | plugin.log_ttyout(slice).map_err(|err| { 258 | let _ = env.stderr().write_error(&err); 259 | err 260 | }) 261 | }) 262 | } 263 | 264 | #[doc(hidden)] 265 | pub unsafe extern "C" fn log_stdin>( 266 | buf: *const raw::c_char, 267 | len: raw::c_uint, 268 | _errstr: *mut *const raw::c_char, 269 | ) -> raw::c_int { 270 | catch_unwind_log(|| { 271 | let env = S::io_env(); 272 | let plugin = S::io_plugin(); 273 | 274 | if !env.command_info.iolog_stdin && !P::IGNORE_IOLOG_HINTS { 275 | return Ok(()); 276 | } 277 | 278 | let slice = ::std::slice::from_raw_parts( 279 | buf.cast(), 280 | len as _, 281 | ); 282 | 283 | plugin.log_stdin(slice).map_err(|err| { 284 | let _ = env.stderr().write_error(&err); 285 | err 286 | }) 287 | }) 288 | } 289 | 290 | #[doc(hidden)] 291 | pub unsafe extern "C" fn log_stdout>( 292 | buf: *const raw::c_char, 293 | len: raw::c_uint, 294 | _errstr: *mut *const raw::c_char, 295 | ) -> raw::c_int { 296 | catch_unwind_log(|| { 297 | let env = S::io_env(); 298 | let plugin = S::io_plugin(); 299 | 300 | if !env.command_info.iolog_stdout && !P::IGNORE_IOLOG_HINTS { 301 | return Ok(()); 302 | } 303 | 304 | let slice = ::std::slice::from_raw_parts( 305 | buf.cast(), 306 | len as _, 307 | ); 308 | 309 | plugin.log_stdout(slice).map_err(|err| { 310 | let _ = env.stderr().write_error(&err); 311 | err 312 | }) 313 | }) 314 | } 315 | 316 | #[doc(hidden)] 317 | pub unsafe extern "C" fn log_stderr>( 318 | buf: *const raw::c_char, 319 | len: raw::c_uint, 320 | _errstr: *mut *const raw::c_char, 321 | ) -> raw::c_int { 322 | catch_unwind_log(|| { 323 | let env = S::io_env(); 324 | let plugin = S::io_plugin(); 325 | 326 | if !env.command_info.iolog_stderr && !P::IGNORE_IOLOG_HINTS { 327 | return Ok(()); 328 | } 329 | 330 | let slice = ::std::slice::from_raw_parts( 331 | buf.cast(), 332 | len as _, 333 | ); 334 | 335 | plugin.log_stderr(slice).map_err(|err| { 336 | let _ = env.stderr().write_error(&err); 337 | err 338 | }) 339 | }) 340 | } 341 | 342 | #[doc(hidden)] 343 | pub unsafe extern "C" fn change_winsize>( 344 | line: raw::c_uint, 345 | cols: raw::c_uint, 346 | _errstr: *mut *const raw::c_char, 347 | ) -> raw::c_int { 348 | catch_unwind_log(|| { 349 | let env = S::io_env(); 350 | let plugin = S::io_plugin(); 351 | 352 | plugin.change_winsize(u64::from(line), u64::from(cols)).map_err(|err| { 353 | let _ = env.stderr().write_error(&err); 354 | err 355 | }) 356 | }) 357 | } 358 | -------------------------------------------------------------------------------- /sudo_plugin/src/errors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | //! The collection of `Error` types used by this library. 16 | 17 | // TODO: use error types as directly defined by sudo_plugin(8). 18 | 19 | use crate::core::{OpenStatus, LogStatus}; 20 | use crate::version::Version; 21 | 22 | use std::result::Result as StdResult; 23 | use std::error::Error as StdError; 24 | use thiserror::Error; 25 | 26 | /// Errors that can be produced by plugin internals. 27 | #[derive(Clone, Debug, Error, PartialEq, Eq)] 28 | pub enum Error { 29 | /// The plugin was called using the conventions of an unsupported 30 | /// API version. 31 | #[error("sudo called plugin with an API version of {provided}, but a minimum of {required} is required")] 32 | UnsupportedApiVersion { 33 | /// The minimum API version supported 34 | required: Version, 35 | 36 | /// The API version provided by `sudo`. 37 | provided: Version, 38 | }, 39 | 40 | /// A required option is missing. 41 | #[error("sudo called plugin without providing a value for {key}")] 42 | OptionMissing { 43 | /// The name of the option. 44 | key: String, 45 | }, 46 | 47 | /// A required option could not be parsed into the expected type. 48 | #[error("sudo called plugin with an unparseable value for {key}: {value}")] 49 | OptionInvalid { 50 | /// The name of the option. 51 | key: String, 52 | 53 | /// The value provided. 54 | value: String, 55 | }, 56 | 57 | /// A plugin method panicked and the panic was captured at the FFI 58 | /// boundary. Panics can't cross into C, so we have to capture it 59 | /// and turn it into an appropriate return code. 60 | #[error("uncaught internal error")] 61 | UncaughtPanic, 62 | 63 | /// A generic error identified only by a provided string. This may 64 | /// be used by plugin implementors who don't wish to provide their 65 | /// own custom error types, and instead are happy to simply use 66 | /// stringly-typed error messages. 67 | #[error("{0}")] 68 | Other(String), 69 | } 70 | 71 | pub(crate) type Result = StdResult; 72 | 73 | /// The type for errors that can be returned from plugin callbacks. 74 | /// Plugin authors are expected to provide an implementation of coercion 75 | /// `From` for their own custom error types 76 | /// as well coercions `Into` and `Into` to 77 | /// specify how those errors should be treated by `sudo`. 78 | pub trait SudoError: StdError + From + Into + Into { } 79 | 80 | impl + Into + Into> SudoError for T {} 81 | 82 | impl From for OpenStatus { 83 | fn from(_: Error) -> Self { 84 | // by default, abort `sudo` on all errors 85 | OpenStatus::Deny 86 | } 87 | } 88 | 89 | impl From for LogStatus { 90 | fn from(_: Error) -> Self { 91 | // by default, abort `sudo` on all errors 92 | LogStatus::Deny 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /sudo_plugin/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | //! description = "Macros to simplify writing sudo plugins" 16 | //! 17 | //! TODO: explain 18 | 19 | // TODO: provide the Plugin object to all callbacks? 20 | 21 | #![warn(future_incompatible)] 22 | #![warn(nonstandard_style)] 23 | #![warn(rust_2021_compatibility)] 24 | #![warn(rust_2018_compatibility)] 25 | #![warn(rust_2018_idioms)] 26 | #![warn(unused)] 27 | 28 | #![warn(bare_trait_objects)] 29 | #![warn(missing_copy_implementations)] 30 | #![warn(missing_debug_implementations)] 31 | #![warn(missing_docs)] 32 | #![warn(single_use_lifetimes)] 33 | #![warn(trivial_casts)] 34 | #![warn(trivial_numeric_casts)] 35 | #![warn(unreachable_pub)] 36 | #![warn(unstable_features)] 37 | #![warn(unused_import_braces)] 38 | #![warn(unused_lifetimes)] 39 | #![warn(unused_qualifications)] 40 | #![warn(unused_results)] 41 | #![warn(variant_size_differences)] 42 | 43 | // this entire crate is unsafe code 44 | #![allow(unsafe_code)] 45 | 46 | #![warn(rustdoc::all)] 47 | 48 | #![warn(clippy::cargo)] 49 | #![warn(clippy::complexity)] 50 | #![warn(clippy::correctness)] 51 | #![warn(clippy::pedantic)] 52 | #![warn(clippy::perf)] 53 | #![warn(clippy::style)] 54 | 55 | // FIXME: this appears to be triggering on non-Drop items like Result, 56 | // but this needs to be either investigated or reported upstream 57 | #![allow(clippy::let_underscore_drop)] 58 | 59 | // this warns on names that are out of our control like argv, argc, uid, 60 | // and gid 61 | #![allow(clippy::similar_names)] 62 | 63 | pub mod core; 64 | pub mod errors; 65 | pub mod options; 66 | pub mod plugin; 67 | pub mod prelude; 68 | 69 | pub mod macros; 70 | 71 | mod output; 72 | mod version; 73 | 74 | pub use sudo_plugin_sys as sys; 75 | -------------------------------------------------------------------------------- /sudo_plugin/src/macros.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | //! Macros to simplify the process of correctly wiring up a sudo plugin. 16 | 17 | /// Emits the boilerplate stanza for creating and initializing a custom 18 | /// sudo I/O plugin. 19 | /// 20 | /// # Example 21 | /// 22 | /// ```rust 23 | /// # mod necessary_for_super_type_lookup_to_work { 24 | /// use sudo_plugin::prelude::*; 25 | /// use std::io::Write; 26 | /// 27 | /// sudo_io_plugin! { example : Example } 28 | /// 29 | /// struct Example { 30 | /// env: &'static IoEnv 31 | /// } 32 | /// 33 | /// impl IoPlugin for Example { 34 | /// type Error = Error; 35 | /// 36 | /// const NAME: &'static str = "example"; 37 | /// 38 | /// fn open(env: &'static IoEnv) -> Result { 39 | /// writeln!(env.stdout(), "example sudo plugin initialized"); 40 | /// 41 | /// Ok(Example { env }) 42 | /// } 43 | /// 44 | /// fn close(self, _: i32, _: i32) { 45 | /// writeln!(self.env.stdout(), "example sudo plugin exited"); 46 | /// } 47 | /// 48 | /// fn log_stdout(&self, _: &[u8]) -> Result<(), Self::Error> { 49 | /// writeln!(self.env.stdout(), "example sudo plugin received output on stdout"); 50 | /// 51 | /// Ok(()) 52 | /// } 53 | /// } 54 | /// # } 55 | /// ``` 56 | /// 57 | /// The generated plugin will have the entry point `example`, so to 58 | /// enable it, you'd copy the library to `example.so` in sudo's plugin 59 | /// directory (on macOS, `/usr/local/libexec/sudo`) and add the following 60 | /// to `/etc/sudo.conf`: 61 | /// 62 | /// ```ignore 63 | /// Plugin example example.so 64 | /// ``` 65 | #[macro_export] 66 | macro_rules! sudo_io_plugin { 67 | ( $name:ident : $ty:ty ) => { 68 | mod $name { 69 | use super::*; 70 | 71 | // TODO: end use of static mut 72 | 73 | static mut SUDO_IO_ENV: Option<$crate::plugin::IoEnv> = None; 74 | static mut SUDO_IO_PLUGIN: Option<$ty> = None; 75 | 76 | pub struct State; 77 | 78 | unsafe impl $crate::plugin::IoState<$ty> for State { 79 | unsafe fn io_env_mut() -> &'static mut Option<$crate::plugin::IoEnv> { 80 | &mut SUDO_IO_ENV 81 | } 82 | 83 | unsafe fn io_plugin_mut() -> &'static mut Option<$ty> { 84 | &mut SUDO_IO_PLUGIN 85 | } 86 | } 87 | } 88 | 89 | // This must be `static mut` as the sudo plugin API writes to 90 | // `event_alloc` to provide us with the address of the function. 91 | #[allow(non_upper_case_globals)] 92 | #[allow(missing_docs)] 93 | #[no_mangle] 94 | pub static mut $name: $crate::sys::io_plugin = $crate::sys::io_plugin { 95 | open: Some($crate::core::open::<$ty, $name::State>), 96 | close: Some($crate::core::close::<$ty, $name::State>), 97 | show_version: Some($crate::core::show_version::<$ty, $name::State>), 98 | 99 | log_ttyin: Some($crate::core::log_ttyin ::<$ty, $name::State>), 100 | log_ttyout: Some($crate::core::log_ttyout::<$ty, $name::State>), 101 | log_stdin: Some($crate::core::log_stdin ::<$ty, $name::State>), 102 | log_stdout: Some($crate::core::log_stdout::<$ty, $name::State>), 103 | log_stderr: Some($crate::core::log_stderr::<$ty, $name::State>), 104 | 105 | change_winsize: Some($crate::core::change_winsize::<$ty, $name::State>), 106 | 107 | .. $crate::sys::io_plugin::empty() 108 | }; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /sudo_plugin/src/options.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | //! Parsers for the various key/value sets of options passed in by 16 | //! `sudo_plugin`. These are parsed into a generic `OptionMap` which is 17 | //! a thin convenience wrapper around a `HashMap, Vec>` 18 | //! (keys and values are not guaranteed to be UTF-8 strings). 19 | //! 20 | //! Once parsed into the generic `OptionMap`, well-known sets of options 21 | //! (`command_info`, `settings`, and `user_info`) are parsed into 22 | //! structs with values of the correct type type (e.g., `user_info.uid` 23 | //! is a `uid_t`). 24 | 25 | // The layouts of the structs below this module aren't under my control. 26 | #![allow(clippy::struct_excessive_bools)] 27 | 28 | #[doc(hidden)] pub mod command_info; 29 | #[doc(hidden)] pub mod option_map; 30 | #[doc(hidden)] pub mod settings; 31 | #[doc(hidden)] pub mod user_info; 32 | 33 | mod traits; 34 | 35 | pub use command_info::CommandInfo; 36 | pub use option_map::OptionMap; 37 | pub use settings::Settings; 38 | pub use user_info::UserInfo; 39 | -------------------------------------------------------------------------------- /sudo_plugin/src/options/command_info.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use crate::errors::{Result, Error}; 16 | use crate::options::OptionMap; 17 | 18 | use std::convert::TryFrom; 19 | use std::os::unix::io::RawFd; 20 | use std::path::PathBuf; 21 | 22 | use libc::{self, gid_t, mode_t, uid_t}; 23 | 24 | /// Information about the command being run. These values are used by 25 | /// sudo to set the execution environment when running a command and set 26 | /// by the policy plugin. 27 | #[derive(Debug)] 28 | pub struct CommandInfo { 29 | /// The root directory to use when running the command. 30 | pub chroot: Option, 31 | 32 | /// If specified, sudo will close all files descriptors with a value 33 | /// of number or higher. 34 | pub close_from: Option, 35 | 36 | /// Fully qualified path to the command to be executed. 37 | pub command: PathBuf, 38 | 39 | /// The current working directory to change to when executing the command. 40 | pub cwd: Option, 41 | 42 | /// By default, sudo runs a command as the foreground process as long as 43 | /// sudo itself is running in the foreground. When exec_background is 44 | /// enabled and the command is being run in a pseudo-terminal (due to I/O 45 | /// logging or the use_pty setting), the command will be run as a 46 | /// background process. Attempts to read from the controlling terminal (or 47 | /// to change terminal settings) will result in the command being suspended 48 | /// with the SIGTTIN signal (or SIGTTOU in the case of terminal settings). 49 | /// If this happens when sudo is a foreground process, the command will be 50 | /// granted the controlling terminal and resumed in the foreground with no 51 | /// user intervention required. The advantage of initially running the 52 | /// command in the background is that sudo need not read from the terminal 53 | /// unless the command explicitly requests it. Otherwise, any terminal 54 | /// input must be passed to the command, whether it has required it or not 55 | /// (the kernel buffers terminals so it is not possible to tell whether the 56 | /// command really wants the input). This is different from historic sudo 57 | /// behavior or when the command is not being run in a pseudo-terminal. 58 | /// 59 | /// For this to work seamlessly, the operating system must support the 60 | /// automatic restarting of system calls. Unfortunately, not all operating 61 | /// systems do this by default, and even those that do may have bugs. For 62 | /// example, macOS fails to restart the tcgetattr() and tcsetattr() system 63 | /// calls (this is a bug in macOS). Furthermore, because this behavior 64 | /// depends on the command stopping with the SIGTTIN or SIGTTOU signals, 65 | /// programs that catch these signals and suspend themselves with a 66 | /// different signal (usually SIGTOP) will not be automatically 67 | /// foregrounded. Some versions of the linux su(1) command behave this way. 68 | /// Because of this, a plugin should not set exec_background unless it is 69 | /// explicitly enabled by the administrator and there should be a way to 70 | /// enabled or disable it on a per-command basis. 71 | /// 72 | /// This setting has no effect unless I/O logging is enabled or use_pty is 73 | /// enabled. 74 | pub exec_background: bool, 75 | 76 | /// If specified, sudo will use the fexecve(2) system call to execute 77 | /// the command instead of execve(2). The specified number must refer to an 78 | /// open file descriptor. 79 | pub exec_fd: Option, 80 | 81 | /// Set to true if the I/O logging plugins, if any, should compress the log 82 | /// data. This is a hint to the I/O logging plugin which may choose to 83 | /// ignore it. 84 | pub iolog_compress: bool, 85 | 86 | /// The group that will own newly created I/O log files and directories. 87 | /// This is a hint to the I/O logging plugin which may choose to ignore it. 88 | pub iolog_group: Option, 89 | 90 | /// The file permission mode to use when creating I/O log files and 91 | /// directories. This is a hint to the I/O logging plugin which may choose 92 | /// to ignore it. 93 | pub iolog_mode: Option, 94 | 95 | /// Fully qualified path to the file or directory in which I/O log is to be 96 | /// stored. This is a hint to the I/O logging plugin which may choose to 97 | /// ignore it. If no I/O logging plugin is loaded, this setting has no 98 | /// effect. 99 | pub iolog_path: Option, 100 | 101 | /// Set to true if the I/O logging plugins, if any, should log the standard 102 | /// input if it is not connected to a terminal device. This is a hint to 103 | /// the I/O logging plugin which may choose to ignore it. 104 | pub iolog_stdin: bool, 105 | 106 | /// Set to true if the I/O logging plugins, if any, should log the standard 107 | /// output if it is not connected to a terminal device. This is a hint to 108 | /// the I/O logging plugin which may choose to ignore it. 109 | pub iolog_stdout: bool, 110 | 111 | /// Set to true if the I/O logging plugins, if any, should log the standard 112 | /// error if it is not connected to a terminal device. This is a hint to 113 | /// the I/O logging plugin which may choose to ignore it. 114 | pub iolog_stderr: bool, 115 | 116 | /// Set to true if the I/O logging plugins, if any, should log all terminal 117 | /// input. This only includes input typed by the user and not from a pipe 118 | /// or redirected from a file. This is a hint to the I/O logging plugin 119 | /// which may choose to ignore it. 120 | pub iolog_ttyin: bool, 121 | 122 | /// Set to true if the I/O logging plugins, if any, should log all terminal 123 | /// output. This only includes output to the screen, not output to a pipe 124 | /// or file. This is a hint to the I/O logging plugin which may choose to 125 | /// ignore it. 126 | pub iolog_ttyout: bool, 127 | 128 | /// The user that will own newly created I/O log files and directories. 129 | /// This is a hint to the I/O logging plugin which may choose to ignore it. 130 | pub iolog_user: Option, 131 | 132 | /// BSD login class to use when setting resource limits and nice value 133 | /// (optional). This option is only set on systems that support login 134 | /// classes. 135 | pub login_class: Option, 136 | 137 | /// Nice value (priority) to use when executing the command. The nice 138 | /// value, if specified, overrides the priority associated with the 139 | /// login_class on BSD systems. 140 | pub nice: Option, 141 | 142 | /// If set, prevent the command from executing other programs. 143 | pub noexec: bool, 144 | 145 | /// A comma-separated list of file descriptors that should be preserved, 146 | /// regardless of the value of the closefrom setting. Only available 147 | /// starting with API version 1.5. 148 | pub preserve_fds: Vec, 149 | 150 | /// If set, sudo will preserve the user's group vector instead of 151 | /// initializing the group vector based on runas_user. 152 | pub preserve_groups: bool, 153 | 154 | /// Effective group-ID to run the command as. If not specified, the value 155 | /// of runas_gid is used. 156 | pub runas_egid: gid_t, 157 | 158 | /// Effective user-ID to run the command as. If not specified, the value of 159 | /// runas_uid is used. 160 | pub runas_euid: uid_t, 161 | 162 | /// Group-ID to run the command as. 163 | pub runas_gid: gid_t, 164 | 165 | /// The supplementary group vector to use for the command in the form of a 166 | /// comma-separated list of group-IDs. If preserve_groups is set, this 167 | /// option is ignored. 168 | pub runas_groups: Option>, 169 | 170 | /// User-ID to run the command as. 171 | pub runas_uid: uid_t, 172 | 173 | /// SELinux role to use when executing the command. 174 | pub selinux_role: Option, 175 | 176 | /// SELinux type to use when executing the command. 177 | pub selinux_type: Option, 178 | 179 | /// Create a utmp (or utmpx) entry when a pseudo-terminal is allocated. By 180 | /// default, the new entry will be a copy of the user's existing utmp entry 181 | /// (if any), with the tty, time, type and pid fields updated. 182 | pub set_utmp: bool, 183 | 184 | /// Set to true when in sudoedit mode. The plugin may enable sudoedit mode 185 | /// even if sudo was not invoked as sudoedit. This allows the plugin to 186 | /// perform command substitution and transparently enable sudoedit when the 187 | /// user attempts to run an editor. 188 | pub sudoedit: bool, 189 | 190 | /// Set to false to disable directory writability checks in sudoedit. By 191 | /// default, sudoedit 1.8.16 and higher will check all directory components 192 | /// of the path to be edited for writability by the invoking user. Symbolic 193 | /// links will not be followed in writable directories and sudoedit will 194 | /// refuse to edit a file located in a writable directory. These 195 | /// restrictions are not enforced when sudoedit is run by root. The 196 | /// sudoedit_follow option can be set to false to disable this check. Only 197 | /// available starting with API version 1.8. 198 | pub sudoedit_checkdir: bool, 199 | 200 | /// Set to true to allow sudoedit to edit files that are symbolic links. By 201 | /// default, sudoedit 1.8.15 and higher will refuse to open a symbolic 202 | /// link. The sudoedit_follow option can be used to restore the older 203 | /// behavior and allow sudoedit to open symbolic links. Only available 204 | /// starting with API version 1.8. 205 | pub sudoedit_follow: bool, 206 | 207 | /// Command timeout. If non-zero then when the timeout expires the command 208 | /// will be killed. 209 | pub timeout: Option, 210 | 211 | /// The file creation mask to use when executing the command. This value 212 | /// may be overridden by PAM or login.conf on some systems unless the 213 | /// umask_override option is also set. 214 | pub umask: mode_t, 215 | 216 | /// Force the value specified by the umask option to override any umask set 217 | /// by PAM or login.conf. 218 | pub umask_override: Option, 219 | 220 | /// Allocate a pseudo-terminal to run the command in, regardless of whether 221 | /// or not I/O logging is in use. By default, sudo will only run the 222 | /// command in a pseudo-terminal when an I/O log plugin is loaded. 223 | pub use_pty: bool, 224 | 225 | /// User name to use when constructing a new utmp (or utmpx) entry when 226 | /// set_utmp is enabled. This option can be used to set the user field in 227 | /// the utmp entry to the user the command runs as rather than the invoking 228 | /// user. If not set, sudo will base the new entry on the invoking user's 229 | /// existing entry. 230 | pub utmp_user: Option, 231 | 232 | /// The raw underlying [`OptionMap`](OptionMap) to retrieve additional 233 | /// values that may not have been known at the time of the authorship of 234 | /// this file. 235 | pub raw: OptionMap, 236 | } 237 | 238 | impl TryFrom for CommandInfo { 239 | type Error = Error; 240 | 241 | fn try_from(value: OptionMap) -> Result { 242 | let runas_gid = value.get("runas_gid") 243 | .unwrap_or_else(|_| unsafe { libc::getegid() }); 244 | 245 | let runas_uid = value.get("runas_uid") 246 | .unwrap_or_else(|_| unsafe { libc::geteuid() }); 247 | 248 | Ok(Self { 249 | // in the event that the `-V` flag is passed to `sudo`, 250 | // there's no command 251 | command: value.get("command").unwrap_or_default(), 252 | runas_gid, 253 | runas_uid, 254 | runas_egid: value.get("runas_egid").unwrap_or(runas_gid), 255 | runas_euid: value.get("runas_euid").unwrap_or(runas_uid), 256 | umask: value.get("umask").unwrap_or(0o7777), 257 | 258 | chroot: value.get("chroot") .ok(), 259 | close_from: value.get("closefrom") .ok(), 260 | cwd: value.get("cwd") .ok(), 261 | exec_background: value.get("exec_background") .unwrap_or(false), 262 | exec_fd: value.get("execfd") .ok(), 263 | iolog_compress: value.get("iolog_compress") .unwrap_or(false), 264 | iolog_group: value.get("iolog_group") .ok(), 265 | iolog_mode: value.get("iolog_mode") .ok(), 266 | iolog_path: value.get("iolog_path") .ok(), 267 | iolog_stdin: value.get("iolog_stdin") .unwrap_or(false), 268 | iolog_stdout: value.get("iolog_stdout") .unwrap_or(false), 269 | iolog_stderr: value.get("iolog_stderr") .unwrap_or(false), 270 | iolog_ttyin: value.get("iolog_ttyin") .unwrap_or(false), 271 | iolog_ttyout: value.get("iolog_ttyout") .unwrap_or(false), 272 | iolog_user: value.get("iolog_user") .ok(), 273 | login_class: value.get("login_class") .ok(), 274 | nice: value.get("nice") .ok(), 275 | noexec: value.get("noexec") .unwrap_or(false), 276 | preserve_fds: value.get("preserve_fds") .unwrap_or_default(), 277 | preserve_groups: value.get("preserve_groups") .unwrap_or(false), 278 | runas_groups: value.get("runas_groups") .ok(), 279 | selinux_role: value.get("selinux_role") .ok(), 280 | selinux_type: value.get("selinux_type") .ok(), 281 | set_utmp: value.get("set_utmp") .unwrap_or(false), 282 | sudoedit: value.get("sudoedit") .unwrap_or(false), 283 | sudoedit_checkdir: value.get("sudoedit_checkdir") .unwrap_or(true), 284 | sudoedit_follow: value.get("sudoedit_follow") .unwrap_or(false), 285 | timeout: value.get("timeout") .ok(), 286 | umask_override: value.get("umask_override") .ok(), 287 | use_pty: value.get("use_pty") .unwrap_or(false), 288 | utmp_user: value.get("utmp_user") .ok(), 289 | 290 | raw: value, 291 | }) 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /sudo_plugin/src/options/option_map.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use crate::errors::{Result, Error}; 16 | use crate::options::traits::FromSudoOption; 17 | 18 | use std::collections::HashMap; 19 | use std::ffi::CStr; 20 | use std::str; 21 | 22 | use libc::c_char; 23 | 24 | const OPTIONS_SEPARATOR: u8 = b'='; 25 | 26 | /// A HashMap-like list of options parsed from the pointers provided by 27 | /// the underlying sudo plugin API. 28 | /// 29 | /// Allows for automatic parsing of values into any type which implements 30 | /// the `FromSudoOption` trait as well as values into a `Vec` of any type 31 | /// which implements the `FromSudoOptionList` trait. 32 | #[derive(Clone, Debug)] 33 | pub struct OptionMap(HashMap, Vec>); 34 | 35 | // TOOD: in policy plugins, some of these values can be written back to 36 | // by the plugin in order to change the execution of sudo itself (e.g., 37 | // `command_info.chroot`, `command_info.cwd`, and others). We should 38 | // support that use-case, but I don't think the current design of the 39 | // API can be plausibly made to support it. This will probably require 40 | // a redesign of this entire thing, which I'm not really excited about. 41 | impl OptionMap { 42 | /// Initializes the `OptionMap` from a pointer to the options 43 | /// provided when `sudo` invokes the plugin's entry function. The 44 | /// format of these is a NUL-terminated array of NUL-terminated 45 | /// strings in "key=value" format with the array terminated by a 46 | /// NULL pointer. 47 | /// 48 | /// # Safety 49 | /// 50 | /// This method cannot be safe, since it relies on the caller to 51 | /// place a NULL byte as the final array entry. In the absence of 52 | /// such a NULL byte, there is no other way to detect the end of 53 | /// the options list. 54 | #[must_use] 55 | pub unsafe fn from_raw(mut ptr: *const *const c_char) -> Self { 56 | let mut map = HashMap::new(); 57 | 58 | // if the pointer is null, we weren't given a list of settings, 59 | // so go ahead and return the empty map 60 | if ptr.is_null() { 61 | return OptionMap(map); 62 | } 63 | 64 | // iterate through each pointer in the array until encountering 65 | // a NULL (which terminates the array) 66 | while !(*ptr).is_null() { 67 | let bytes = CStr::from_ptr(*ptr).to_bytes(); 68 | let sep = bytes.iter().position(|b| *b == OPTIONS_SEPARATOR); 69 | 70 | // separators might not exist (e.g., in the case of parsing 71 | // plugin options; for this case, we use the full entry as 72 | // both the name of the key and its value 73 | // 74 | // indexing into an array can panic, but there's no cleaner 75 | // way in rust to split a byte array on a delimiter, and 76 | // the code above selects `sep` such that it's guaranteed to 77 | // be within the slice 78 | let (k, v) = match sep { 79 | Some(s) => { ( &bytes[..s], &bytes[s+1..] ) } 80 | None => { ( bytes, bytes) } 81 | }; 82 | 83 | // the return value is ignored because there's not an 84 | // otherwise-reasonable way to handle duplicate key names; 85 | // that said, the implications of this are that the last 86 | // value of a given key is the one that's set 87 | drop(map.insert( 88 | k.to_owned(), 89 | v.to_owned(), 90 | )); 91 | 92 | ptr = ptr.offset(1); 93 | } 94 | 95 | OptionMap(map) 96 | } 97 | 98 | /// Gets the value of a key as any arbitrary type that implements the 99 | /// `FromSudoOption` trait. Returns `Err(_)` if no such key/value-pair 100 | /// was provided during initialization. Also returns `Err(_)` if the 101 | /// value was not interpretable as a UTF-8 string or if there was an 102 | /// error parsing the value to the requested type. 103 | /// 104 | /// # Errors 105 | /// 106 | /// Returns an error if the option could not be successfully parsed 107 | /// into type `T`. 108 | pub fn get(&self, k: &str) -> Result { 109 | let v = self.get_str(k).ok_or(Error::OptionMissing { key: k.into() })?; 110 | 111 | FromSudoOption::from_sudo_option(v) 112 | .map_err(|_e| Error::OptionInvalid { key: k.into(), value: v.into() }) 113 | } 114 | 115 | /// Gets the value of a key as a string. Returns `None` if no such 116 | /// key/value-pair was provided during initialization. Also returns 117 | /// `None` if the value was not interpretable as a UTF-8 string. 118 | #[must_use] 119 | pub fn get_str(&self, k: &str) -> Option<&str> { 120 | self.get_bytes(k.as_bytes()).and_then(|b| str::from_utf8(b).ok()) 121 | } 122 | 123 | /// Fetches a raw byte value using a bytes as the key. This is 124 | /// provided to allow plugins to retrieve values for keys when the 125 | /// value and/or key are not guaranteed to be UTF-8 strings. 126 | pub fn get_bytes(&self, k: &[u8]) -> Option<&[u8]> { 127 | self.0.get(k).map(Vec::as_slice) 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | use crate::options::traits::FromSudoOptionList; 135 | 136 | use std::collections::HashSet; 137 | use std::path::PathBuf; 138 | use std::ptr; 139 | 140 | impl FromSudoOptionList for String { 141 | const SEPARATOR: char = '|'; 142 | } 143 | 144 | #[test] 145 | fn new_parses_string_keys() { 146 | let map = unsafe { OptionMap::from_raw([ 147 | b"key1=value1\0".as_ptr() as _, 148 | b"key2=value2\0".as_ptr() as _, 149 | ptr::null(), 150 | ].as_ptr()) }; 151 | 152 | assert_eq!("value1", map.get_str("key1").unwrap()); 153 | assert_eq!("value2", map.get_str("key2").unwrap()); 154 | assert!(map.get_str("key3").is_none()); 155 | } 156 | 157 | #[test] 158 | fn new_parses_null_options() { 159 | let map = unsafe { OptionMap::from_raw(ptr::null()) }; 160 | 161 | assert!(map.0.is_empty()) 162 | } 163 | 164 | #[test] 165 | fn new_parses_non_utf8_keys() { 166 | let map = unsafe { OptionMap::from_raw([ 167 | b"\x80=value\0".as_ptr() as _, 168 | ptr::null(), 169 | ].as_ptr()) }; 170 | 171 | assert_eq!(&b"value"[..], map.get_bytes(b"\x80").unwrap()); 172 | } 173 | 174 | #[test] 175 | fn new_parses_non_utf8_values() { 176 | let map = unsafe { OptionMap::from_raw([ 177 | b"key=\x80\0".as_ptr() as _, 178 | ptr::null(), 179 | ].as_ptr()) }; 180 | 181 | assert_eq!(None, map.get_str("key")); 182 | assert_eq!(&b"\x80"[..], map.get_bytes(b"key").unwrap()); 183 | } 184 | 185 | #[test] 186 | fn new_parses_repeated_keys() { 187 | let map = unsafe { OptionMap::from_raw([ 188 | b"key=value1\0".as_ptr() as _, 189 | b"key=value2\0".as_ptr() as _, 190 | b"key=value3\0".as_ptr() as _, 191 | ptr::null(), 192 | ].as_ptr()) }; 193 | 194 | assert_eq!("value3", map.get_str("key").unwrap()); 195 | } 196 | 197 | #[test] 198 | fn new_parses_valueless_keys() { 199 | let map = unsafe { OptionMap::from_raw([ 200 | b"key\0".as_ptr() as _, 201 | ptr::null(), 202 | ].as_ptr()) }; 203 | 204 | assert_eq!("key", map.get_str("key").unwrap()); 205 | } 206 | 207 | #[test] 208 | fn new_parses_values_with_the_separator() { 209 | let map = unsafe { OptionMap::from_raw([ 210 | b"key=value=value\0".as_ptr() as _, 211 | ptr::null(), 212 | ].as_ptr()) }; 213 | 214 | assert_eq!("value=value", map.get_str("key").unwrap()); 215 | assert_eq!(None, map.get_str("key=value")); 216 | } 217 | 218 | #[test] 219 | fn get_parses_common_types() { 220 | let map = unsafe { OptionMap::from_raw([ 221 | b"str=value\0" .as_ptr() as _, 222 | b"true\0" .as_ptr() as _, 223 | b"false\0" .as_ptr() as _, 224 | b"i64=-42\0" .as_ptr() as _, 225 | b"u64=42\0" .as_ptr() as _, 226 | b"path=/foo/bar\0".as_ptr() as _, 227 | ptr::null(), 228 | ].as_ptr()) }; 229 | 230 | assert_eq!(String::from("value"), map.get::("str").unwrap()); 231 | assert_eq!(PathBuf::from("/foo/bar"), map.get::("path").unwrap()); 232 | 233 | assert_eq!(true, map.get::("true") .unwrap()); 234 | assert_eq!(false, map.get::("false").unwrap()); 235 | assert!(map.get::("str").is_err()); 236 | 237 | assert_eq!(-42, map.get::("i64") .unwrap()); 238 | assert_eq!(-42, map.get::("i64").unwrap()); 239 | assert_eq!(-42, map.get::("i64").unwrap()); 240 | assert_eq!(-42, map.get::("i64").unwrap()); 241 | assert!(map.get::("true").is_err()); 242 | 243 | assert_eq!(42, map.get::("u64") .unwrap()); 244 | assert_eq!(42, map.get::("u64").unwrap()); 245 | assert_eq!(42, map.get::("u64").unwrap()); 246 | assert_eq!(42, map.get::("u64").unwrap()); 247 | assert!(map.get::("i64").is_err()); 248 | } 249 | 250 | #[test] 251 | fn get_parses_lists() { 252 | let map = unsafe { OptionMap::from_raw([ 253 | b"ints=1,2,3\0".as_ptr() as _, 254 | b"strs=a|b|c\0".as_ptr() as _, 255 | b"str=a,b,c\0" .as_ptr() as _, 256 | ptr::null(), 257 | ].as_ptr()) }; 258 | 259 | assert_eq!(vec![1, 2, 3], map.get::>("ints") .unwrap()); 260 | assert_eq!(vec!["a", "b", "c"], map.get::>("strs").unwrap()); 261 | assert_eq!(vec!["a,b,c"], map.get::>("str") .unwrap()); 262 | } 263 | 264 | #[test] 265 | fn get_parses_hashsets() { 266 | 267 | let map = unsafe { OptionMap::from_raw([ 268 | b"nums=1|2|3\0".as_ptr() as _, 269 | ptr::null(), 270 | ].as_ptr()) }; 271 | 272 | let set = map.get::>("nums").unwrap(); 273 | 274 | assert!(set.contains("1")); 275 | assert!(set.contains("2")); 276 | assert!(set.contains("3")); 277 | assert!(!set.contains("4")); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /sudo_plugin/src/options/settings.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use crate::errors::{Result, Error}; 16 | use crate::options::OptionMap; 17 | use crate::options::traits::{FromSudoOption, FromSudoOptionList}; 18 | 19 | use std::convert::TryFrom; 20 | use std::net::{AddrParseError, IpAddr}; 21 | use std::str; 22 | 23 | /// A vector of user-supplied sudo settings. These settings correspond 24 | /// to options the user specified when running sudo. As such, they will 25 | /// only be present when the corresponding option has been specified on 26 | /// the command line. 27 | #[derive(Debug)] 28 | pub struct Settings { 29 | /// Authentication type, if specified by the -a option, to use on systems 30 | /// where BSD authentication is supported. 31 | pub bsd_auth_type: Option, 32 | 33 | /// If specified, the user has requested via the -C option that sudo close 34 | /// all files descriptors with a value of number or higher. The plugin may 35 | /// optionally pass this, or another value, back in the command_info list. 36 | pub close_from: Option, 37 | 38 | /// A debug file path name followed by a space and a comma-separated list 39 | /// of debug flags that correspond to the plugin's Debug entry in 40 | /// sudo.conf(5), if there is one. The flags are passed to the plugin 41 | /// exactly as they appear in sudo.conf(5). The syntax used by sudo and the 42 | /// sudoers plugin is subsystem@priority but a plugin is free to use a 43 | /// different format so long as it does not include a comma (‘,’). Prior to 44 | /// sudo 1.8.12, there was no way to specify plugin-specific debug_flags so 45 | /// the value was always the same as that used by the sudo front end and 46 | /// did not include a path name, only the flags themselves. As of version 47 | /// 1.7 of the plugin interface, sudo will only pass debug_flags if 48 | /// sudo.conf(5) contains a plugin-specific Debug entry. 49 | pub debug_flags: Option, 50 | 51 | /// This setting has been deprecated in favor of debug_flags. 52 | pub debug_level: Option, 53 | 54 | /// Set to true if the user specified the -k option along with a command, 55 | /// indicating that the user wishes to ignore any cached authentication 56 | /// credentials. implied_shell to true. This allows sudo with no arguments 57 | /// to be used similarly to su(1). If the plugin does not to support this 58 | /// usage, it may return a value of -2 from the check_policy() function, 59 | /// which will cause sudo to print a usage message and exit. 60 | pub ignore_ticket: bool, 61 | 62 | /// If the user does not specify a program on the command line, sudo will 63 | /// pass the plugin the path to the user's shell and set 64 | pub implied_shell: bool, 65 | 66 | /// BSD login class to use when setting resource limits and nice value, if 67 | /// specified by the -c option. 68 | pub login_class: Option, 69 | 70 | /// Set to true if the user specified the -i option, indicating that the 71 | /// user wishes to run a login shell. 72 | pub login_shell: bool, 73 | 74 | /// The maximum number of groups a user may belong to. This will only be 75 | /// present if there is a corresponding setting in sudo.conf(5). 76 | pub max_groups: Option, 77 | 78 | /// A space-separated list of IP network addresses and netmasks in the form 79 | /// “addr/netmask”, e.g., “192.168.1.2/255.255.255.0”. The address and 80 | /// netmask pairs may be either IPv4 or IPv6, depending on what the 81 | /// operating system supports. If the address contains a colon (‘:’), it is 82 | /// an IPv6 address, else it is IPv4. 83 | pub network_addrs: Vec, 84 | 85 | /// Set to true if the user specified the -n option, indicating that sudo 86 | /// should operate in non-interactive mode. The plugin may reject a command 87 | /// run in non-interactive mode if user interaction is required. 88 | pub noninteractive: bool, 89 | 90 | /// The default plugin directory used by the sudo front end. This is the 91 | /// default directory set at compile time and may not correspond to the 92 | /// directory the running plugin was loaded from. It may be used by a 93 | /// plugin to locate support files. 94 | pub plugin_dir: String, 95 | 96 | /// The path name of plugin loaded by the sudo front end. The path name 97 | /// will be a fully-qualified unless the plugin was statically compiled 98 | /// into sudo. 99 | pub plugin_path: String, 100 | 101 | /// Set to true if the user specified the -E option, indicating that the 102 | /// user wishes to preserve the environment. 103 | pub preserve_environment: bool, 104 | 105 | /// Set to true if the user specified the -P option, indicating that the 106 | /// user wishes to preserve the group vector instead of setting it based on 107 | /// the runas user. 108 | pub preserve_groups: bool, 109 | 110 | /// The command name that sudo was run as, typically “sudo” or “sudoedit”. 111 | pub progname: String, 112 | 113 | /// The prompt to use when requesting a password, if specified via the -p 114 | /// option. 115 | pub prompt: Option, 116 | 117 | /// The name of the remote host to run the command on, if specified via the 118 | /// -h option. Support for running the command on a remote host is meant to 119 | /// be implemented via a helper program that is executed in place of the 120 | /// user-specified command. The sudo front end is only capable of executing 121 | /// commands on the local host. Only available starting with API version 122 | /// 1.4. 123 | pub remote_host: Option, 124 | 125 | /// Set to true if the user specified the -s option, indicating that the 126 | /// user wishes to run a shell. 127 | pub run_shell: bool, 128 | 129 | /// The group name or gid to run the command as, if specified via the -g 130 | /// option. 131 | pub runas_group: Option, 132 | 133 | /// The user name or uid to run the command as, if specified via the -u 134 | /// option. 135 | pub runas_user: Option, 136 | 137 | /// SELinux role to use when executing the command, if specified by the -r 138 | /// option. 139 | pub selinux_role: Option, 140 | 141 | /// SELinux type to use when executing the command, if specified by the -t 142 | /// option. 143 | pub selinux_type: Option, 144 | 145 | /// Set to true if the user specified the -H option. If true, set the HOME 146 | /// environment variable to the target user's home directory. 147 | pub set_home: bool, 148 | 149 | /// Set to true when the -e option is specified or if invoked as sudoedit. 150 | /// The plugin shall substitute an editor into argv in the check_policy() 151 | /// function or return -2 with a usage error if the plugin does not support 152 | /// sudoedit. For more information, see the check_policy section. 153 | pub sudoedit: bool, 154 | 155 | /// User-specified command timeout. Not all plugins support command 156 | /// timeouts and the ability for the user to set a timeout may be 157 | /// restricted by policy. The format of the timeout string is 158 | /// plugin-specific. 159 | pub timeout: Option, 160 | 161 | /// The raw underlying [`OptionMap`](OptionMap) to retrieve additional 162 | /// values that may not have been known at the time of the authorship of 163 | /// this file. 164 | pub raw: OptionMap, 165 | } 166 | 167 | impl Settings { 168 | // TODO: surely this can be made more cleanly; also, it would be 169 | // great if we could actually get the full original `sudo` 170 | // invocation without having to reconstruct it by hand 171 | // 172 | // TODO: maybe if /proc/$$/cmd exists I can prefer to use it 173 | #[must_use] 174 | pub fn flags(&self) -> Vec> { 175 | let mut flags: Vec> = vec![]; 176 | 177 | // `sudoedit` is set if the flag was provided *or* if sudo 178 | // was invoked as `sudoedit` directly; try our best to intrepret 179 | // this case, although we'll technically get it wrong in the 180 | // case of `sudoedit -e ...` 181 | if self.sudoedit && self.progname != "sudoedit" { 182 | flags.push(b"--edit".to_vec()); 183 | } 184 | 185 | if let Some(ref runas_user) = self.runas_user { 186 | let mut flag = b"--user ".to_vec(); 187 | flag.extend_from_slice(runas_user.as_bytes()); 188 | 189 | flags.push(flag); 190 | } 191 | 192 | if let Some(ref runas_group) = self.runas_group { 193 | let mut flag = b"--group ".to_vec(); 194 | flag.extend_from_slice(runas_group.as_bytes()); 195 | 196 | flags.push(flag); 197 | } 198 | 199 | if let Some(ref prompt) = self.prompt { 200 | let mut flag = b"--prompt ".to_vec(); 201 | flag.extend_from_slice(prompt.as_bytes()); 202 | 203 | flags.push(flag); 204 | } 205 | 206 | if self.login_shell { 207 | flags.push(b"--login".to_vec()); 208 | } 209 | 210 | if self.run_shell { 211 | flags.push(b"--shell".to_vec()); 212 | } 213 | 214 | if self.set_home { 215 | flags.push(b"--set-home".to_vec()); 216 | } 217 | 218 | if self.preserve_environment { 219 | flags.push(b"--preserve-env".to_vec()); 220 | } 221 | 222 | if self.preserve_groups { 223 | flags.push(b"--preserve-groups".to_vec()); 224 | } 225 | 226 | if self.ignore_ticket { 227 | flags.push(b"--reset-timestamp".to_vec()); 228 | } 229 | 230 | if self.noninteractive { 231 | flags.push(b"--non-interactive".to_vec()); 232 | } 233 | 234 | if let Some(ref login_class) = self.login_class { 235 | let mut flag = b"--login-class ".to_vec(); 236 | flag.extend_from_slice(login_class.as_bytes()); 237 | 238 | flags.push(flag); 239 | } 240 | 241 | if let Some(ref selinux_role) = self.selinux_role { 242 | let mut flag = b"--role ".to_vec(); 243 | flag.extend_from_slice(selinux_role.as_bytes()); 244 | 245 | flags.push(flag); 246 | } 247 | 248 | if let Some(ref selinux_type) = self.selinux_type { 249 | let mut flag = b"--type ".to_vec(); 250 | flag.extend_from_slice(selinux_type.as_bytes()); 251 | 252 | flags.push(flag); 253 | } 254 | 255 | if let Some(ref bsd_auth_type) = self.bsd_auth_type { 256 | let mut flag = b"--auth-type ".to_vec(); 257 | flag.extend_from_slice(bsd_auth_type.as_bytes()); 258 | 259 | flags.push(flag); 260 | } 261 | 262 | if let Some(close_from) = self.close_from { 263 | let mut flag = b"--close-from ".to_vec(); 264 | flag.extend_from_slice(close_from.to_string().as_bytes()); 265 | 266 | flags.push(flag); 267 | } 268 | 269 | flags 270 | } 271 | } 272 | 273 | impl TryFrom for Settings { 274 | type Error = Error; 275 | 276 | fn try_from(value: OptionMap) -> Result { 277 | Ok(Self { 278 | plugin_dir: value.get("plugin_dir")?, 279 | plugin_path: value.get("plugin_path")?, 280 | progname: value.get("progname")?, 281 | 282 | bsd_auth_type: value.get("bsd_auth_type") .ok(), 283 | close_from: value.get("closefrom") .ok(), 284 | debug_flags: value.get("debug_flags") .ok(), 285 | debug_level: value.get("debug_level") .ok(), 286 | ignore_ticket: value.get("ignore_ticket") .unwrap_or(false), 287 | implied_shell: value.get("implied_shell") .unwrap_or(false), 288 | login_class: value.get("login_class") .ok(), 289 | login_shell: value.get("login_shell") .unwrap_or(false), 290 | max_groups: value.get("max_groups") .ok(), 291 | network_addrs: value.get("network_addrs") .unwrap_or_else(|_| vec![]), 292 | noninteractive: value.get("noninteractive") .unwrap_or(false), 293 | preserve_environment: value.get("preserve_environment").unwrap_or(false), 294 | preserve_groups: value.get("preserve_groups") .unwrap_or(false), 295 | prompt: value.get("prompt") .ok(), 296 | remote_host: value.get("remote_host") .ok(), 297 | run_shell: value.get("run_shell") .unwrap_or(false), 298 | runas_group: value.get("runas_group") .ok(), 299 | runas_user: value.get("runas_user") .ok(), 300 | selinux_role: value.get("selinux_role") .ok(), 301 | selinux_type: value.get("selinux_type") .ok(), 302 | set_home: value.get("set_home") .unwrap_or(false), 303 | sudoedit: value.get("sudoedit") .unwrap_or(false), 304 | timeout: value.get("timeout") .ok(), 305 | 306 | raw: value, 307 | }) 308 | } 309 | } 310 | 311 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 312 | pub struct NetAddr { 313 | pub addr: IpAddr, 314 | pub mask: IpAddr, 315 | } 316 | 317 | impl FromSudoOption for NetAddr { 318 | type Err = AddrParseError; 319 | 320 | // indexing into an array can panic, but there's no cleaner way in 321 | // rust to split a byte array on a delimiter, and the code below 322 | // selects the midpoint such that it's guaranteed to be within the 323 | // slice 324 | fn from_sudo_option(s: &str) -> ::std::result::Result { 325 | let bytes = s.as_bytes(); 326 | let mid = bytes.iter() 327 | .position(|b| *b == b'/' ) 328 | .unwrap_or_else(|| bytes.len()); 329 | 330 | let addr = s[ .. mid].parse()?; 331 | let mask = s[mid + 1 .. ].parse()?; 332 | 333 | Ok(Self { 334 | addr, 335 | mask, 336 | }) 337 | } 338 | } 339 | 340 | impl FromSudoOptionList for NetAddr { 341 | const SEPARATOR: char = ' '; 342 | } 343 | -------------------------------------------------------------------------------- /sudo_plugin/src/options/traits.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use std::collections::HashSet; 16 | use std::hash::Hash; 17 | use std::path::PathBuf; 18 | use std::str::FromStr; 19 | 20 | #[derive(Clone, Copy, Debug)] 21 | pub struct ParseListError(T); 22 | 23 | pub trait FromSudoOption: Sized { 24 | type Err; 25 | 26 | fn from_sudo_option(s: &str) -> ::std::result::Result; 27 | } 28 | 29 | impl FromSudoOption for bool { 30 | type Err = ::std::str::ParseBoolError; 31 | 32 | fn from_sudo_option(s: &str) -> ::std::result::Result { 33 | FromStr::from_str(s) 34 | } 35 | } 36 | 37 | impl FromSudoOption for i8 { 38 | type Err = ::std::num::ParseIntError; 39 | 40 | fn from_sudo_option(s: &str) -> ::std::result::Result { 41 | FromStr::from_str(s) 42 | } 43 | } 44 | 45 | impl FromSudoOption for u8 { 46 | type Err = ::std::num::ParseIntError; 47 | 48 | fn from_sudo_option(s: &str) -> ::std::result::Result { 49 | FromStr::from_str(s) 50 | } 51 | } 52 | 53 | impl FromSudoOption for i16 { 54 | type Err = ::std::num::ParseIntError; 55 | 56 | fn from_sudo_option(s: &str) -> ::std::result::Result { 57 | FromStr::from_str(s) 58 | } 59 | } 60 | 61 | 62 | impl FromSudoOption for u16 { 63 | type Err = ::std::num::ParseIntError; 64 | 65 | fn from_sudo_option(s: &str) -> ::std::result::Result { 66 | FromStr::from_str(s) 67 | } 68 | } 69 | 70 | impl FromSudoOption for i32 { 71 | type Err = ::std::num::ParseIntError; 72 | 73 | fn from_sudo_option(s: &str) -> ::std::result::Result { 74 | FromStr::from_str(s) 75 | } 76 | } 77 | 78 | impl FromSudoOption for u32 { 79 | type Err = ::std::num::ParseIntError; 80 | 81 | fn from_sudo_option(s: &str) -> ::std::result::Result { 82 | FromStr::from_str(s) 83 | } 84 | } 85 | 86 | impl FromSudoOption for i64 { 87 | type Err = ::std::num::ParseIntError; 88 | 89 | fn from_sudo_option(s: &str) -> ::std::result::Result { 90 | FromStr::from_str(s) 91 | } 92 | } 93 | 94 | impl FromSudoOption for u64 { 95 | type Err = ::std::num::ParseIntError; 96 | 97 | fn from_sudo_option(s: &str) -> ::std::result::Result { 98 | FromStr::from_str(s) 99 | } 100 | } 101 | 102 | impl FromSudoOption for String { 103 | type Err = ::std::string::ParseError; 104 | 105 | fn from_sudo_option(s: &str) -> ::std::result::Result { 106 | FromStr::from_str(s) 107 | } 108 | } 109 | 110 | impl FromSudoOption for PathBuf { 111 | type Err = ::std::string::ParseError; 112 | 113 | fn from_sudo_option(s: &str) -> ::std::result::Result { 114 | Ok(s.into()) 115 | } 116 | } 117 | 118 | impl FromSudoOption for Vec 119 | where 120 | T: FromSudoOption + FromSudoOptionList, 121 | { 122 | type Err = ParseListError; 123 | 124 | fn from_sudo_option(s: &str) -> ::std::result::Result { 125 | let list = ::from_sudo_option_list(s); 126 | let mut items = Self::with_capacity(list.len()); 127 | 128 | for element in list { 129 | let item = FromSudoOption::from_sudo_option(element) 130 | .map_err(ParseListError)?; 131 | 132 | items.push(item); 133 | } 134 | 135 | Ok(items) 136 | } 137 | } 138 | 139 | impl FromSudoOption for HashSet 140 | where 141 | T: Eq + Hash + FromSudoOption + FromSudoOptionList, 142 | { 143 | type Err = ParseListError; 144 | 145 | fn from_sudo_option(s: &str) -> ::std::result::Result { 146 | Vec::::from_sudo_option(s).map(|vec| vec.into_iter().collect()) 147 | } 148 | } 149 | 150 | pub trait FromSudoOptionList: Sized { 151 | const SEPARATOR: char = ','; 152 | 153 | fn from_sudo_option_list(s: &str) -> Vec<&str> { 154 | s.split(|b| b == Self::SEPARATOR).collect() 155 | } 156 | } 157 | 158 | impl FromSudoOptionList for i8 {} 159 | impl FromSudoOptionList for u8 {} 160 | impl FromSudoOptionList for i16 {} 161 | impl FromSudoOptionList for u16 {} 162 | impl FromSudoOptionList for i32 {} 163 | impl FromSudoOptionList for u32 {} 164 | impl FromSudoOptionList for i64 {} 165 | impl FromSudoOptionList for u64 {} 166 | impl FromSudoOptionList for PathBuf {} 167 | -------------------------------------------------------------------------------- /sudo_plugin/src/options/user_info.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use crate::errors::{Result, Error}; 16 | use crate::options::OptionMap; 17 | 18 | use std::convert::TryFrom; 19 | use std::path::PathBuf; 20 | 21 | use libc::{gid_t, pid_t, uid_t}; 22 | 23 | /// A vector of information about the user running the command. 24 | #[derive(Debug)] 25 | pub struct UserInfo { 26 | /// The number of columns the user's terminal supports. If there is no 27 | /// terminal device available, a default value of 80 is used. 28 | pub cols: u64, 29 | 30 | /// The user's current working directory. 31 | pub cwd: PathBuf, 32 | 33 | /// The effective group-ID of the user invoking sudo. 34 | pub egid: gid_t, 35 | 36 | /// The effective user-ID of the user invoking sudo. 37 | pub euid: uid_t, 38 | 39 | /// The real group-ID of the user invoking sudo. 40 | pub gid: gid_t, 41 | 42 | /// The user's supplementary group list formatted as a string of 43 | /// comma-separated group-IDs. 44 | pub groups: Vec, 45 | 46 | /// The local machine's hostname as returned by the gethostname(2) system 47 | /// call. 48 | pub host: String, 49 | 50 | /// The number of lines the user's terminal supports. If there is no 51 | /// terminal device available, a default value of 24 is used. 52 | pub lines: u64, 53 | 54 | /// The ID of the process group that the running sudo process is a member 55 | /// of. Only available starting with API version 1.2. 56 | pub pgid: pid_t, 57 | 58 | /// The process ID of the running sudo process. Only available starting 59 | /// with API version 1.2. 60 | pub pid: pid_t, 61 | 62 | /// The parent process ID of the running sudo process. Only available 63 | /// starting with API version 1.2. 64 | pub ppid: pid_t, 65 | 66 | /// The session ID of the running sudo process or 0 if sudo is not part of 67 | /// a POSIX job control session. Only available starting with API version 68 | /// 1.2. 69 | pub sid: pid_t, 70 | 71 | /// The ID of the foreground process group associated with the terminal 72 | /// device associated with the sudo process or -1 if there is no terminal 73 | /// present. Only available starting with API version 1.2. 74 | pub tcpgid: pid_t, 75 | 76 | /// The path to the user's terminal device. If the user has no terminal 77 | /// device associated with the session, the value will be empty, as in 78 | /// “tty=”. 79 | pub tty: Option, 80 | 81 | /// The real user-ID of the user invoking sudo. 82 | pub uid: uid_t, 83 | 84 | /// The invoking user's file creation mask. Only available starting with 85 | /// API version 1.10. 86 | pub umask: Option, 87 | 88 | /// The name of the user invoking sudo. 89 | pub user: String, 90 | 91 | /// The raw underlying [`OptionMap`](OptionMap) to retrieve additional 92 | /// values that may not have been known at the time of the authorship of 93 | /// this file. 94 | pub raw: OptionMap, 95 | } 96 | 97 | impl TryFrom for UserInfo { 98 | type Error = Error; 99 | 100 | fn try_from(value: OptionMap) -> Result { 101 | Ok(Self { 102 | cwd: value.get("cwd")?, 103 | egid: value.get("egid")?, 104 | euid: value.get("euid")?, 105 | gid: value.get("gid")?, 106 | groups: value.get("groups")?, 107 | host: value.get("host")?, 108 | pgid: value.get("pgid")?, 109 | pid: value.get("pid")?, 110 | ppid: value.get("ppid")?, 111 | uid: value.get("uid")?, 112 | user: value.get("user")?, 113 | 114 | umask: value.get("umask") .ok(), 115 | cols: value.get("cols") .unwrap_or(80), 116 | lines: value.get("lines") .unwrap_or(24), 117 | sid: value.get("sid") .unwrap_or(0), 118 | tcpgid: value.get("tcpgid").unwrap_or(-1), 119 | tty: value.get("tty") .ok(), 120 | 121 | raw: value, 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /sudo_plugin/src/output.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | pub(crate) mod print_facility; 16 | pub(crate) mod tty; 17 | 18 | pub(crate) use print_facility::PrintFacility; 19 | pub(crate) use tty::Tty; 20 | 21 | use crate::sys; 22 | 23 | #[derive(Clone, Copy, Debug)] 24 | #[repr(u32)] 25 | enum Level { 26 | Info = sys::SUDO_CONV_INFO_MSG, 27 | Error = sys::SUDO_CONV_ERROR_MSG, 28 | } 29 | -------------------------------------------------------------------------------- /sudo_plugin/src/output/print_facility.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use crate::output::Level; 16 | 17 | use sudo_plugin_sys::sudo_printf_t; 18 | 19 | use std::error::Error; 20 | use std::ffi::CString; 21 | use std::io::{self, Write}; 22 | 23 | /// A facility implementing `std::io::Write` that allows printing 24 | /// output to the user invoking `sudo`. Technically, the user may 25 | /// not be present on a local tty, but this will be wired up to a 26 | /// `printf`-like function that outputs to either STDOUT or STDERR. 27 | #[derive(Clone, Debug)] 28 | pub struct PrintFacility { 29 | /// A function pointer to an underlying printf facility provided 30 | /// by the sudo_plugin API. 31 | facility: sudo_printf_t, 32 | 33 | /// The [`Level`] to send messages at. The sudo_plugin API only 34 | /// supports distinguishing between informational messages and error 35 | /// messages. 36 | level: Level, 37 | 38 | /// An optional tag to prepend to any logged messages. 39 | tag: Vec, 40 | } 41 | 42 | impl PrintFacility { 43 | /// Constructs a new `PrintFacility` that emits output to the user invoking 44 | /// `sudo`. A tuple is returned that presents a handle to write to `stdout` 45 | /// and `stderr` in that order. 46 | /// 47 | /// An optional `name` can be provided. If it is, the [`write_line`] and 48 | /// [`write_error`] methods will emit the name as a prefix for each line. 49 | /// 50 | /// # Safety 51 | /// 52 | /// This function *must* be provided with either a `None` or a real pointer 53 | /// to a `printf`-style function. Once provided to this function, the 54 | /// function pointer should be discarded at never used, as it is unsafe for 55 | /// this function to be called concurrently. 56 | #[must_use] 57 | pub unsafe fn new(name: Option<&str>, printf: sudo_printf_t) -> (Self, Self) { 58 | let tag: Vec = name 59 | .map(|name| format!("{}: ", name).into()) 60 | .unwrap_or_default(); 61 | 62 | let stdout = Self { tag, facility: printf, level: Level::Info }; 63 | let mut stderr = stdout.clone(); 64 | 65 | stderr.level = Level::Error; 66 | 67 | (stdout, stderr) 68 | } 69 | 70 | /// Pretty-prints a line, prefixed by the name of the plugin. 71 | pub fn write_line(&mut self, line: &[u8]) -> io::Result<()> { 72 | let tag = self.tag.clone(); 73 | 74 | self.write_all(tag.as_slice())?; 75 | self.write_all(line)?; 76 | self.write_all(b"\n")?; 77 | 78 | Ok(()) 79 | } 80 | 81 | /// Pretty-prints nested errors to the user. 82 | pub fn write_error(&mut self, error: &dyn Error) -> io::Result<()> { 83 | // errors are prefixed with a newline for clarity, since they 84 | // might be emitted while an existing line has output on it 85 | self.write_all(b"\n")?; 86 | 87 | // TODO: replace this and the `while` with error.chain() when 88 | // stabilized 89 | self.write_line(format!("{}", error).as_bytes())?; 90 | 91 | while let Some(error) = error.source() { 92 | self.write_line(format!("{}", error).as_bytes())?; 93 | } 94 | 95 | Ok(()) 96 | } 97 | } 98 | 99 | impl Write for PrintFacility { 100 | fn write(&mut self, buf: &[u8]) -> io::Result { 101 | let printf = self.facility.ok_or_else(|| 102 | io::Error::new(io::ErrorKind::NotConnected, "no printf provided") 103 | )?; 104 | 105 | let message = CString::new(buf).map_err(|err| 106 | io::Error::new(io::ErrorKind::InvalidData, err) 107 | )?; 108 | 109 | let count = unsafe { 110 | // TODO: level should be bitflags when we start implementing the 111 | // full conversation interface 112 | (printf)(self.level as _, message.as_ptr()) 113 | }; 114 | 115 | #[allow(clippy::cast_sign_loss)] 116 | match count { 117 | c if c < 0 => Err(io::Error::last_os_error()), 118 | c => Ok(c as _) 119 | } 120 | } 121 | 122 | fn flush(&mut self) -> io::Result<()> { 123 | Ok(()) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /sudo_plugin/src/output/tty.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use std::convert::TryFrom; 16 | use std::fs::{File, OpenOptions}; 17 | use std::io::{self, Write}; 18 | use std::path::Path; 19 | 20 | /// 21 | /// A facility implementing `std::io::Write` that allows printing 22 | /// output to directly to the terminal of the user invoking `sudo`. 23 | /// 24 | #[derive(Debug)] 25 | pub struct Tty(File); 26 | 27 | impl TryFrom<&Path> for Tty { 28 | type Error = io::Error; 29 | 30 | fn try_from(path: &Path) -> io::Result { 31 | OpenOptions::new().write(true).open(path).map(Tty) 32 | } 33 | } 34 | 35 | impl Write for Tty { 36 | fn write(&mut self, buf: &[u8]) -> io::Result { 37 | self.0.write(buf) 38 | } 39 | 40 | fn flush(&mut self) -> io::Result<()> { 41 | self.0.flush() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sudo_plugin/src/plugin.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | //! Traits and structs directly used for implementation of plugins. 16 | 17 | #![allow(clippy::module_name_repetitions)] 18 | 19 | mod io_env; 20 | mod io_plugin; 21 | mod io_state; 22 | 23 | pub use io_env::IoEnv; 24 | pub use io_plugin::IoPlugin; 25 | pub use io_state::IoState; 26 | -------------------------------------------------------------------------------- /sudo_plugin/src/plugin/io_env.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use crate::errors::Result; 16 | use crate::version::Version; 17 | use crate::options::{OptionMap, CommandInfo, Settings, UserInfo}; 18 | use crate::output::{PrintFacility, Tty}; 19 | 20 | use std::convert::{TryFrom, TryInto}; 21 | use std::collections::HashSet; 22 | use std::path::PathBuf; 23 | use std::ffi::{CString, CStr}; 24 | use std::slice; 25 | 26 | use libc::{c_char, c_int, c_uint, gid_t}; 27 | 28 | /// An implementation of the sudo [`io_plugin`](crate::sys::io_plugin) environment, initialized 29 | /// and parsed from the values passed to the underlying `open` callback. 30 | #[allow(missing_debug_implementations)] 31 | pub struct IoEnv { 32 | /// The name of the plugin. This will be the generally be the same 33 | /// as the name of the exported C struct. 34 | pub plugin_name: &'static str, 35 | 36 | /// The version of the plugin. 37 | pub plugin_version: &'static str, 38 | 39 | /// The plugin API version supported by the invoked `sudo` command. 40 | pub api_version: Version, 41 | 42 | /// The command being executed under `sudo`, in the same form as 43 | /// would be passed to the `execve(2)` system call. 44 | pub cmdline: Vec, 45 | 46 | /// A map of user-supplied sudo settings. These settings correspond 47 | /// to flags the user specified when running sudo. As such, they 48 | /// will only be present when the corresponding flag has been specified 49 | /// on the command line. 50 | pub settings: Settings, 51 | 52 | /// A map of information about the user running the command. 53 | pub user_info: UserInfo, 54 | 55 | /// A map of information about the command being run. 56 | pub command_info: CommandInfo, 57 | 58 | /// A map of the user's environment variables. 59 | pub user_env: OptionMap, 60 | 61 | /// A map of options provided to the plugin after the its path in 62 | /// sudo.conf. 63 | /// 64 | /// Settings that aren't of the form `key=value` will have a key 65 | /// in the map whose value is the same as the key, similar to how 66 | /// HTML handles valueless attributes (e.g., `disabled` will become 67 | /// `plugin_options["disabled"] => "disabled"`). 68 | pub plugin_options: OptionMap, 69 | 70 | /// A handle to the plugin's printf_facility, configured to write to 71 | /// the user's stdout. 72 | stdout: PrintFacility, 73 | 74 | /// A handle to the plugin's printf_facility, configured to write to 75 | /// the user's stdin. 76 | stderr: PrintFacility, 77 | 78 | /// A (currently-unused) handle to the sudo_plugin conversation 79 | /// facility, which allows two-way communication with the user. 80 | _conversation: crate::sys::sudo_conv_t, 81 | } 82 | 83 | // I don't get to control how many arguments these methods accept, since 84 | // it's dictated by the C plugin. 85 | #[allow(clippy::too_many_arguments)] 86 | impl IoEnv { 87 | /// Initializes an `IoEnv` from the arguments provided to the 88 | /// underlying C `open` callback function. 89 | /// 90 | /// Verifies that the API version advertised by the underlying 91 | /// `sudo` is supported, parses all provided options, and wires up 92 | /// communication facilities. 93 | // 94 | /// # Errors 95 | /// 96 | /// Returns an error if there was a problem initializing the plugin. 97 | /// 98 | /// # Safety 99 | /// 100 | /// This function is inherently unsafe since it's provided with 101 | /// raw pointers. As long as sudo obeys its contracts for how these 102 | /// are interpreted (see [`OptionMap`](OptionMap) for details). 103 | pub unsafe fn new( 104 | plugin_name: &'static str, 105 | plugin_version: &'static str, 106 | version: c_uint, 107 | argc: c_int, 108 | argv: *const *mut c_char, 109 | settings: *const *mut c_char, 110 | user_info: *const *mut c_char, 111 | command_info: *const *mut c_char, 112 | user_env: *const *mut c_char, 113 | plugin_options: *const *mut c_char, 114 | plugin_printf: crate::sys::sudo_printf_t, 115 | conversation: crate::sys::sudo_conv_t, 116 | ) -> Result { 117 | let version = Version::from(version).check()?; 118 | 119 | let (stdout, stderr) = PrintFacility::new( 120 | Some(plugin_name), 121 | plugin_printf 122 | ); 123 | 124 | // god help us all if `argc` is negative 125 | #[allow(clippy::cast_sign_loss)] 126 | let mut argv = slice::from_raw_parts( 127 | argv, 128 | argc as usize 129 | ).to_vec(); 130 | 131 | let cmdline = argv 132 | .iter_mut() 133 | .map(|ptr| CStr::from_ptr(*ptr).to_owned()) 134 | .collect(); 135 | 136 | let plugin = Self { 137 | plugin_name, 138 | plugin_version, 139 | 140 | api_version: version, 141 | 142 | cmdline, 143 | 144 | settings: OptionMap::from_raw(settings.cast()).try_into()?, 145 | user_info: OptionMap::from_raw(user_info.cast()).try_into()?, 146 | command_info: OptionMap::from_raw(command_info.cast()).try_into()?, 147 | user_env: OptionMap::from_raw(user_env.cast()), 148 | plugin_options: OptionMap::from_raw(plugin_options.cast()), 149 | 150 | stdout, 151 | stderr, 152 | _conversation: conversation, 153 | }; 154 | 155 | Ok(plugin) 156 | } 157 | 158 | /// 159 | /// Returns a facility implementing `std::io::Write` that emits to 160 | /// the invoking user's STDOUT. 161 | /// 162 | #[must_use] 163 | pub fn stdout(&self) -> PrintFacility { 164 | self.stdout.clone() 165 | } 166 | 167 | /// 168 | /// Returns a facility implementing `std::io::Write` that emits to 169 | /// the invoking user's STDERR. 170 | /// 171 | #[must_use] 172 | pub fn stderr(&self) -> PrintFacility { 173 | self.stderr.clone() 174 | } 175 | 176 | /// 177 | /// Returns a facility implementing `std::io::Write` that emits to 178 | /// the user's TTY, if sudo detected one. 179 | /// 180 | #[must_use] 181 | pub fn tty(&self) -> Option { 182 | self.user_info.tty.as_ref().and_then(|path| 183 | Tty::try_from(path.as_path()).ok() 184 | ) 185 | } 186 | 187 | /// 188 | /// As best as can be reconstructed, what was actually typed at the 189 | /// shell in order to launch this invocation of sudo. 190 | /// 191 | // TODO: I don't really like this name 192 | #[must_use] 193 | pub fn invocation(&self) -> Vec { 194 | let mut sudo = self.settings.progname.as_bytes().to_vec(); 195 | let flags = self.settings.flags(); 196 | 197 | if !flags.is_empty() { 198 | sudo.push(b' '); 199 | sudo.extend_from_slice(&flags.join(&b' ')[..]); 200 | } 201 | 202 | for entry in &self.cmdline { 203 | sudo.push(b' '); 204 | sudo.extend_from_slice(entry.as_bytes()); 205 | } 206 | 207 | sudo 208 | } 209 | 210 | /// 211 | /// The `cwd` to be used for the command being run. This is 212 | /// typically set on the `user_info` component, but may be 213 | /// overridden by the policy plugin setting its value on 214 | /// `command_info`. 215 | /// 216 | #[must_use] 217 | pub fn cwd(&self) -> &PathBuf { 218 | self.command_info.cwd.as_ref().unwrap_or( 219 | &self.user_info.cwd 220 | ) 221 | } 222 | 223 | /// 224 | /// The complete set of groups the invoked command will have 225 | /// privileges for. If the `-P` (`--preserve-groups`) flag was 226 | /// passed to `sudo`, the underlying `command_info` will not have 227 | /// this set and this method will return the list of original groups 228 | /// from the running the command. 229 | /// 230 | /// This set will always contain `runas_egid`. 231 | /// 232 | #[must_use] 233 | pub fn runas_gids(&self) -> HashSet { 234 | // sanity-check that if preserve_groups is unset we have 235 | // `runas_groups`, and if it is set that we don't 236 | if self.command_info.preserve_groups { 237 | debug_assert!(self.command_info.runas_groups.is_none()); 238 | } else { 239 | debug_assert!(self.command_info.runas_groups.is_some()); 240 | } 241 | 242 | // even though the above sanity-check might go wrong, it still 243 | // seems like a safe bet that if `runas_groups` isn't set that 244 | // the command will be invoked with the original user's groups 245 | // (it will probably require reading the `sudo` source code to 246 | // verify this) 247 | let mut set : HashSet<_> = self.command_info.runas_groups.as_ref().unwrap_or( 248 | &self.user_info.groups 249 | ).iter().copied().collect(); 250 | 251 | // `command_info.runas_egid` won't necessarily be in the list of 252 | // `command_info.runas_groups` if `-P` was passed; however, the 253 | // user will have this in the list of groups that they will gain 254 | // permissions for so it seems sane to include it in this list 255 | let _ = set.insert(self.command_info.runas_egid); 256 | 257 | set 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /sudo_plugin/src/plugin/io_plugin.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // TODO: once associated type defaults are stabilized, provide defaults 16 | // for `Error`, and use that to return `Disable` for default methods in 17 | // order to avoid any performance penalties from calling unimplemented 18 | // functions. 19 | 20 | use super::IoEnv; 21 | use crate::errors::SudoError; 22 | 23 | #[warn(clippy::missing_inline_in_public_items)] 24 | 25 | /// The trait that defines the implementation of a sudo I/O plugin. 26 | pub trait IoPlugin: 'static + Sized { 27 | /// The type for errors returned by this `IoPlugin`. Errors must 28 | /// implement the [`SudoError`](crate::errors::SudoError) trait 29 | /// which describes how to convert them to the return codes expected 30 | /// by the `sudo_plugin(8)` facility. 31 | type Error: SudoError; 32 | 33 | /// The name of the plugin. Used when printing the version of the 34 | /// plugin and error messages. 35 | const NAME: &'static str; 36 | 37 | /// The version of the plugin. Defaults to the the value of the 38 | /// `CARGO_PKG_VERSION` environment variable during build. 39 | const VERSION: &'static str = env!("CARGO_PKG_VERSION"); 40 | 41 | /// The `sudo_plugin` facility sets `iolog_{facility}` hints that I 42 | /// believe come from whether or not `LOG_INPUT` or `LOG_OUTPUT` are 43 | /// set. By default, plugins will not have their logging callbacks 44 | /// invoked if `sudo` has told us not to log. 45 | /// 46 | /// Setting this to `true` will ignore these hints and always call 47 | /// user-provided `log_*` callbacks. 48 | const IGNORE_IOLOG_HINTS: bool = false; 49 | 50 | /// Prints the name and version of the plugin. A default 51 | /// implementation of this function is provided, but may be 52 | /// overridden if desired. 53 | #[inline] 54 | fn show_version(env: &IoEnv, _verbose: bool) { 55 | use std::io::Write; 56 | 57 | let _ = writeln!( 58 | env.stdout(), 59 | "{} I/O plugin version {}", 60 | Self::NAME, 61 | Self::VERSION, 62 | ); 63 | } 64 | 65 | /// The `open` function is run before the `log_ttyin`, `log_ttyout`, 66 | /// `log_stdin`, `log_stdout`, `log_stderr`, `log_suspend`, or 67 | /// `change_winsize` methods are called. It is only called if the 68 | /// policy plugin's `check_policy` function has returned 69 | /// successfully. 70 | /// 71 | /// # Errors 72 | /// 73 | /// Any errors will be recursively printed (up their 74 | /// [`source`](std::error::Error::source) chain) then converted to 75 | /// an [`OpenStatus`](crate::errors::OpenStatus) before being 76 | /// returned to `sudo`. 77 | fn open(env: &'static IoEnv) -> Result; 78 | 79 | /// The `close` method is called when the command being run by 80 | /// sudo finishes. A default no-op implementation is provided, but 81 | /// be overriden if desired. 82 | /// 83 | /// As suggested by its signature, once this method exits, the 84 | /// plugin will be dropped. 85 | #[inline] 86 | fn close(self, _exit_status: i32, _error: i32) {} 87 | 88 | /// The `log_ttyin` method is called whenever data can be read from the user 89 | /// but before it is passed to the running command. This allows the plugin 90 | /// to reject data if it chooses to (for instance if the input contains 91 | /// banned content). 92 | /// 93 | /// # Errors 94 | /// 95 | /// Any errors will be recursively printed (up their 96 | /// [`source`](std::error::Error::source) chain) then converted to 97 | /// a [`LogStatus`](crate::errors::LogStatus) before being 98 | /// returned to `sudo`. 99 | #[inline] 100 | fn log_ttyin(&self, _log: &[u8]) -> Result<(), Self::Error> { 101 | Ok(()) 102 | } 103 | 104 | /// The `log_ttyout` function is called whenever data can be read from the 105 | /// command but before it is written to the user's terminal. This allows the 106 | /// plugin to reject data if it chooses to (for instance if the output 107 | /// contains banned content). 108 | /// 109 | /// # Errors 110 | /// 111 | /// Any errors will be recursively printed (up their 112 | /// [`source`](std::error::Error::source) chain) then converted to 113 | /// a [`LogStatus`](crate::errors::LogStatus) before being 114 | /// returned to `sudo`. 115 | #[inline] 116 | fn log_ttyout(&self, _log: &[u8]) -> Result<(), Self::Error> { 117 | Ok(()) 118 | } 119 | 120 | /// The `log_stdin` function is only used if the standard input does not 121 | /// correspond to a tty device. It is called whenever data can be read from 122 | /// the standard input but before it is passed to the running command. 123 | /// 124 | /// # Errors 125 | /// 126 | /// Any errors will be recursively printed (up their 127 | /// [`source`](std::error::Error::source) chain) then converted to 128 | /// a [`LogStatus`](crate::errors::LogStatus) before being 129 | /// returned to `sudo`. 130 | #[inline] 131 | fn log_stdin(&self, _log: &[u8]) -> Result<(), Self::Error> { 132 | Ok(()) 133 | } 134 | 135 | /// The `log_stdout` function is only used if the standard output does not 136 | /// correspond to a tty device. It is called whenever data can be read from 137 | /// the command but before it is written to the standard output. This allows 138 | /// the plugin to reject data if it chooses to (for instance if the output 139 | /// contains banned content). 140 | /// 141 | /// # Errors 142 | /// 143 | /// Any errors will be recursively printed (up their 144 | /// [`source`](std::error::Error::source) chain) then converted to 145 | /// a [`LogStatus`](crate::errors::LogStatus) before being 146 | /// returned to `sudo`. 147 | #[inline] 148 | fn log_stdout(&self, _log: &[u8]) -> Result<(), Self::Error> { 149 | Ok(()) 150 | } 151 | 152 | /// The `log_stderr` function is only used if the standard error does not 153 | /// correspond to a tty device. It is called whenever data can be read from 154 | /// the command but before it is written to the standard error. This allows 155 | /// the plugin to reject data if it chooses to (for instance if the output 156 | /// contains banned content). 157 | /// 158 | /// # Errors 159 | /// 160 | /// Any errors will be recursively printed (up their 161 | /// [`source`](std::error::Error::source) chain) then converted to 162 | /// a [`LogStatus`](crate::errors::LogStatus) before being 163 | /// returned to `sudo`. 164 | #[inline] 165 | fn log_stderr(&self, _log: &[u8]) -> Result<(), Self::Error> { 166 | Ok(()) 167 | } 168 | 169 | /// The `change_winsize` callback is invoked whenever the controlling 170 | /// terminal for the sudo session detects that it's been resized. It is 171 | /// provided with a count of the number of horizontal lines and vertical 172 | /// columns that the terminal can now display. 173 | /// 174 | /// # Errors 175 | /// 176 | /// Any errors will be recursively printed (up their 177 | /// [`source`](std::error::Error::source) chain) then converted to 178 | /// a [`LogStatus`](crate::errors::LogStatus) before being 179 | /// returned to `sudo`. 180 | #[inline] 181 | fn change_winsize(&self, _lines: u64, _cols: u64) -> Result<(), Self::Error> { 182 | Ok(()) 183 | } 184 | 185 | // TODO: support for `log_suspend` 186 | } 187 | -------------------------------------------------------------------------------- /sudo_plugin/src/plugin/io_state.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use super::{IoEnv, IoPlugin}; 16 | use crate::core::OpenStatus; 17 | 18 | /// This trait is implemented invisibly by the `sudo_io_plugin` macro 19 | /// and is not user-visible. 20 | /// 21 | /// # Safety 22 | /// 23 | /// Implementing this trait is unsafe since it operates directly on 24 | /// mutable static state. Efforts are currently underway to minimize the 25 | /// total surface area of this unsafety. 26 | #[doc(hidden)] 27 | pub unsafe trait IoState { 28 | /// Returns a mutable borrow to a static location of the 29 | /// [`IoEnv`](IoEnv). 30 | /// 31 | /// # Safety 32 | /// 33 | /// This function is inherently unsafe since it returns a mutable 34 | /// reference to static memory. `sudo_plugin` never uses threads, so 35 | /// as long as implementors don't this will effectively be safe. 36 | unsafe fn io_env_mut() -> &'static mut Option; 37 | 38 | /// Returns a mutable borrow to a static location of the type 39 | /// implementing [`IoPlugin`](IoPlugin). 40 | /// 41 | /// # Safety 42 | /// 43 | /// This function is inherently unsafe since it returns a mutable 44 | /// reference to static memory. `sudo_plugin` never uses threads, so 45 | /// as long as implementors don't this will effectively be safe. 46 | unsafe fn io_plugin_mut() -> &'static mut Option

; 47 | 48 | /// Initializes the static [`IoEnv`](IoEnv) containing the plugin 49 | /// environment. Takes a callback that is provided a reference to 50 | /// this environment and which must return a fully initialized 51 | /// [`IoPlugin`](IoPlugin). 52 | // 53 | // TODO: It feels leaky for this to care about OpenStatus. 54 | fn init, F: FnOnce(&'static IoEnv) -> Result> ( 55 | env: IoEnv, 56 | f: F, 57 | ) -> OpenStatus { 58 | unsafe { 59 | let env = Self::io_env_mut().get_or_insert(env); 60 | let plugin = f(env).map(|p| Self::io_plugin_mut().get_or_insert(p)); 61 | 62 | match plugin { 63 | Ok(_) => OpenStatus::Ok, 64 | Err(e) => e.into(), 65 | } 66 | } 67 | } 68 | 69 | /// Drops the static [`IoPlugin`](IoPlugin) and [`IoEnv`](IoEnv) so 70 | /// they are properly cleaned up. 71 | /// 72 | /// # Safety 73 | /// 74 | /// This method is unsafe if anything is still holding on to the 75 | /// static references returned by other methods of this trait. 76 | /// Obviously this is Very Bad(TM), so this trait is being reworked 77 | /// to not provide access to mutable static data. 78 | unsafe fn drop(f: F) { 79 | // SAFETY: it's extremely important that the plugin be dropped 80 | // *before* the environment is dropped, because the plugin may 81 | // hold onto a reference to the static environment 82 | let _ = Self::io_plugin_mut().take().map(f); 83 | let _ = Self::io_env_mut() .take(); 84 | } 85 | 86 | /// Returns an immutable borrow to the static location of the 87 | /// contained [`IoEnv`](IoEnv). If the `IoState` has not been 88 | /// initialized yet, this method will panic. 89 | #[must_use] 90 | fn io_env() -> &'static IoEnv { 91 | unsafe { 92 | Self::io_env_mut().as_ref().expect("plugin environment was not initialized") 93 | } 94 | } 95 | 96 | /// Returns an immutable borrow to the static location of the 97 | /// contained [`IoPlugin`](IoPlugin). If the `IoState` has not been 98 | /// initialized yet, this method will panic. 99 | #[must_use] 100 | fn io_plugin() -> &'static P { 101 | unsafe { 102 | Self::io_plugin_mut().as_ref().expect("plugin was not initialized") 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /sudo_plugin/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | //! A prelude module that includes most of what's necessary to start 16 | //! using this crate. 17 | 18 | pub use crate::core::{OpenStatus, LogStatus}; 19 | pub use crate::errors::Error; 20 | pub use crate::options::OptionMap; 21 | pub use crate::plugin::{IoEnv, IoPlugin}; 22 | pub use crate::sudo_io_plugin; 23 | -------------------------------------------------------------------------------- /sudo_plugin/src/version.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Square Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | use crate::errors::{Result, Error}; 16 | use crate::sys; 17 | 18 | use std::fmt; 19 | use std::os::raw::c_uint; 20 | 21 | const MINIMUM: Version = Version::new(1, 9); 22 | 23 | #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] 24 | pub struct Version { 25 | major: u16, 26 | minor: u16, 27 | } 28 | 29 | impl Version { 30 | pub const fn new(major: u16, minor: u16) -> Self { 31 | Self { major, minor } 32 | } 33 | 34 | pub const fn from_ffi(version: c_uint) -> Self { 35 | // this cast is guaranteed not to truncate thanks to the shifts 36 | // and masks 37 | #[allow(clippy::cast_possible_truncation)] 38 | Self::new( 39 | sys::sudo_api_version_get_major(version) as _, 40 | sys::sudo_api_version_get_minor(version) as _, 41 | ) 42 | } 43 | 44 | pub const fn into_ffi(self) -> c_uint { 45 | sys::sudo_api_mkversion( 46 | self.major as _, 47 | self.minor as _, 48 | ) 49 | } 50 | 51 | pub const fn minimum() -> &'static Self { 52 | &MINIMUM 53 | } 54 | 55 | pub fn supported(self) -> bool { 56 | self >= *Self::minimum() 57 | } 58 | 59 | pub fn check(self) -> Result { 60 | if !self.supported() { 61 | return Err(Error::UnsupportedApiVersion { 62 | required: MINIMUM, 63 | provided: self, 64 | }); 65 | } 66 | 67 | Ok(self) 68 | } 69 | } 70 | 71 | impl From for Version { 72 | fn from(version: c_uint) -> Self { 73 | Self::from_ffi(version) 74 | } 75 | } 76 | 77 | impl From for c_uint { 78 | fn from(version: Version) -> Self { 79 | version.into_ffi() 80 | } 81 | } 82 | 83 | impl fmt::Display for Version { 84 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 85 | write!(f, "{}.{}", self.major, self.minor) 86 | } 87 | } 88 | --------------------------------------------------------------------------------