Repository: memorysafety/sudo-rs
Pull request #123 (2023-03-21 09:51:54)

PAM module shakedown
* Very simple (but nice to have) test that the errno-to-int and vice versa conversion function are consistent
* Exercising the PAM conversation function (let's give Miri something to do)

*Update*
* Fixes a flaw in pam_msgs handling
* Removes any aliasing (Rust rules are on flux on this, but we can avoid it altogether)

codecov[bot]: ## [Codecov](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety) Report
Patch coverage: **`100.00`**% and project coverage change: **`+2.64`** :tada:
> Comparison is base [(`53e4fd6`)](https://codecov.io/gh/memorysafety/sudo-rs/commit/53e4fd64ebaa276be9e59dcdac81556f2cf1d0d6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety) 63.69% compared to head [(`659630b`)](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety) 66.34%.

> :exclamation: Current head 659630b differs from pull request most recent head eb97a52. Consider uploading reports for the commit eb97a52 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #123      +/-   ##
==========================================
+ Coverage   63.69%   66.34%   +2.64%     
==========================================
  Files          27       28       +1     
  Lines        3033     3078      +45     
==========================================
+ Hits         1932     2042     +110     
+ Misses       1101     1036      -65     
```


| [Impacted Files](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety) | Coverage Δ | |
|---|---|---|
| [lib/sudo-pam/src/error.rs](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety#diff-bGliL3N1ZG8tcGFtL3NyYy9lcnJvci5ycw==) | `81.63% <100.00%> (+81.63%)` | :arrow_up: |

... and [4 files with indirect coverage changes](https://codecov.io/gh/memorysafety/sudo-rs/pull/123/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety)

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety).

codecov[bot]: ## [Codecov](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety) Report
Patch coverage: **`100.00`**% and project coverage change: **`-2.19`** :warning:
> Comparison is base [(`61868ff`)](https://codecov.io/gh/memorysafety/sudo-rs/commit/61868ffb1ccd8a140993e7ccd6eae0ce73da9e93?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety) 68.52% compared to head [(`659630b`)](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety) 66.34%.

> :exclamation: Current head 659630b differs from pull request most recent head 37d8e49. Consider uploading reports for the commit 37d8e49 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #123      +/-   ##
==========================================
- Coverage   68.52%   66.34%   -2.19%     
==========================================
  Files          28       28              
  Lines        3180     3078     -102     
==========================================
- Hits         2179     2042     -137     
- Misses       1001     1036      +35     
```


| [Impacted Files](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety) | Coverage Δ | |
|---|---|---|
| [lib/sudo-pam/src/error.rs](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety#diff-bGliL3N1ZG8tcGFtL3NyYy9lcnJvci5ycw==) | `81.63% <100.00%> (+81.63%)` | :arrow_up: |

... and [7 files with indirect coverage changes](https://codecov.io/gh/memorysafety/sudo-rs/pull/123/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety)

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/memorysafety/sudo-rs/pull/123?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=memorysafety).


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 06803277..ff7e4489 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -183,7 +183,6 @@ jobs:
       - name: Run clippy
         uses: actions-rs/clippy-check@v1
         with:
-          name: clippy-result
           token: ${{ secrets.GITHUB_TOKEN }}
           args: --no-deps
 
@@ -267,7 +266,7 @@ jobs:
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2
         with:
-          shared-key: "audit"
+          shared-key: "stable"
       - name: Run audit
         uses: actions-rs/cargo@v1
         with:
diff --git a/Cargo.lock b/Cargo.lock
index 4c85f33f..25542a4b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -20,7 +20,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 1.0.109",
+ "syn",
  "which",
 ]
 
@@ -73,22 +73,12 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.1.14"
+version = "4.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "906f7fe1da4185b7a282b2bc90172a496f9def1aca4545fe7526810741591e14"
-dependencies = [
- "clap_builder",
- "clap_derive",
- "once_cell",
-]
-
-[[package]]
-name = "clap_builder"
-version = "4.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "351f9ad9688141ed83dfd8f5fb998a06225ef444b48ff4dc43de6d409b7fd10b"
+checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5"
 dependencies = [
  "bitflags",
+ "clap_derive",
  "clap_lex",
  "is-terminal",
  "once_cell",
@@ -98,27 +88,31 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.1.14"
+version = "4.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81d7dc0031c3a59a04fc2ba395c8e2dd463cba1859275f065d225f6122221b45"
+checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0"
 dependencies = [
  "heck",
+ "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 2.0.11",
+ "syn",
 ]
 
 [[package]]
 name = "clap_lex"
-version = "0.4.0"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f0807fb6f644c83f3e4ec014fec9858c1c8b26a7db8eb5f0bde5817df9c1df7"
+checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09"
+dependencies = [
+ "os_str_bytes",
+]
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.6"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
 dependencies = [
  "libc",
 ]
@@ -140,7 +134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
 dependencies = [
  "quote",
- "syn 1.0.109",
+ "syn",
 ]
 
 [[package]]
@@ -188,9 +182,9 @@ dependencies = [
 
 [[package]]
 name = "generic-array"
-version = "0.14.7"
+version = "0.14.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
 dependencies = [
  "typenum",
  "version_check",
@@ -216,20 +210,19 @@ checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
 
 [[package]]
 name = "io-lifetimes"
-version = "1.0.9"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb"
+checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3"
 dependencies = [
- "hermit-abi",
  "libc",
  "windows-sys",
 ]
 
 [[package]]
 name = "is-terminal"
-version = "0.4.5"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e"
+checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857"
 dependencies = [
  "hermit-abi",
  "io-lifetimes",
@@ -308,6 +301,12 @@ version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
 
+[[package]]
+name = "os_str_bytes"
+version = "6.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
+
 [[package]]
 name = "output_vt100"
 version = "0.1.3"
@@ -335,38 +334,62 @@ dependencies = [
  "yansi",
 ]
 
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
 [[package]]
 name = "proc-macro2"
-version = "1.0.54"
+version = "1.0.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.26"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
 dependencies = [
  "proc-macro2",
 ]
 
 [[package]]
 name = "regex"
-version = "1.7.3"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
 dependencies = [
  "regex-syntax",
 ]
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.29"
+version = "0.6.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
 
 [[package]]
 name = "rpassword"
@@ -397,9 +420,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustix"
-version = "0.36.11"
+version = "0.36.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e"
+checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc"
 dependencies = [
  "bitflags",
  "errno",
@@ -481,6 +504,7 @@ dependencies = [
  "sudo-cli",
  "sudo-pam",
  "sudo-system",
+ "sudoers",
  "thiserror",
  "which",
 ]
@@ -564,17 +588,6 @@ dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "syn"
-version = "2.0.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
 [[package]]
 name = "termcolor"
 version = "1.2.0"
@@ -595,22 +608,22 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.40"
+version = "1.0.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
+checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.40"
+version = "1.0.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
+checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.11",
+ "syn",
 ]
 
 [[package]]
@@ -684,9 +697,9 @@ dependencies = [
 
 [[package]]
 name = "windows-targets"
-version = "0.42.2"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
 dependencies = [
  "windows_aarch64_gnullvm",
  "windows_aarch64_msvc",
@@ -699,45 +712,45 @@ dependencies = [
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.42.2"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.42.2"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.42.2"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.42.2"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.42.2"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.42.2"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.42.2"
+version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
 
 [[package]]
 name = "yansi"
diff --git a/lib/sudo-common/Cargo.toml b/lib/sudo-common/Cargo.toml
index 9d60824f..094056fb 100644
--- a/lib/sudo-common/Cargo.toml
+++ b/lib/sudo-common/Cargo.toml
@@ -11,6 +11,7 @@ publish.workspace = true
 sudo-cli.workspace = true
 sudo-pam.workspace = true
 sudo-system.workspace = true
+sudoers.workspace = true
 libc.workspace = true
 thiserror.workspace = true
 which.workspace = true
diff --git a/lib/sudo-common/src/context.rs b/lib/sudo-common/src/context.rs
index 53582485..a74a930d 100644
--- a/lib/sudo-common/src/context.rs
+++ b/lib/sudo-common/src/context.rs
@@ -1,6 +1,11 @@
-use std::{collections::HashMap, path::PathBuf, str::FromStr};
+use std::{
+    collections::{HashMap, HashSet},
+    path::PathBuf,
+    str::FromStr,
+};
 use sudo_cli::SudoOptions;
 use sudo_system::{hostname, Group, User};
+use sudoers::Settings;
 
 use crate::error::Error;
 
@@ -20,15 +25,8 @@ impl<'a> TryFrom<&'a [String]> for CommandAndArguments<'a> {
 
         let command = iter.next().ok_or(Error::InvalidCommand)?.to_string();
 
-        // resolve the binary if the path is not absolute
-        let command = if command.starts_with('/') {
-            PathBuf::from(command)
-        } else {
-            // TODO: we resolve in the context of the current user using the 'which' crate - we want to reconsider this in the future
-            // TODO: use value of secure_path setting to possible override current path
-            // FIXME: propagating the error is a possible security problem since it leaks information before any permission check is done
-            which::which(command).map_err(|_| Error::InvalidCommand)?
-        };
+        // TODO: we resolve in the context of the current user using the 'which' crate - we want to reconsider this in the future
+        let command = which::which(command).map_err(|_| Error::InvalidCommand)?;
 
         Ok(CommandAndArguments {
             command,
@@ -73,24 +71,38 @@ pub struct Context<'a> {
     pub pid: i32,
 }
 
+pub trait Configuration {
+    fn env_keep(&self) -> &HashSet<String>;
+    fn env_check(&self) -> &HashSet<String>;
+}
+
+impl Configuration for Settings {
+    fn env_keep(&self) -> &HashSet<String> {
+        self.list
+            .get("env_keep")
+            .expect("env_keep missing from settings")
+    }
+
+    fn env_check(&self) -> &HashSet<String> {
+        self.list
+            .get("env_check")
+            .expect("env_check missing from settings")
+    }
+}
+
 fn resolve_current_user() -> Result<User, Error> {
     User::real()?.ok_or(Error::UserNotFound("current user".to_string()))
 }
 
 fn resolve_target_user(target_name_or_id: &Option<String>) -> Result<User, Error> {
-    let is_default = target_name_or_id.is_none();
     let target_name_or_id = target_name_or_id.as_deref().unwrap_or("root");
 
-    let mut user = match NameOrId::parse(target_name_or_id) {
+    match NameOrId::parse(target_name_or_id) {
         Some(NameOrId::Name(name)) => User::from_name(name)?,
         Some(NameOrId::Id(uid)) => User::from_uid(uid)?,
         _ => None,
     }
-    .ok_or_else(|| Error::UserNotFound(target_name_or_id.to_string()))?;
-
-    user.is_default = is_default;
-
-    Ok(user)
+    .ok_or_else(|| Error::UserNotFound(target_name_or_id.to_string()))
 }
 
 fn resolve_target_group(
@@ -179,7 +191,6 @@ mod tests {
             shell: "/bin/sh".to_string(),
             passwd: String::new(),
             groups: None,
-            is_default: false,
         };
 
         assert_eq!(
diff --git a/lib/sudo-defaults/src/lib.rs b/lib/sudo-defaults/src/lib.rs
index 2b10828a..8925d1a1 100644
--- a/lib/sudo-defaults/src/lib.rs
+++ b/lib/sudo-defaults/src/lib.rs
@@ -1,9 +1,10 @@
 // FUTURE IDEA: use a representation that allows for more Rust-type structure rather than passing
 // strings around; some settings in sudoers file are more naturally represented like that, such as
 // "verifypw" and "logfile"
+#[derive(Debug)]
 pub enum SudoDefault {
     Flag(bool),
-    Integer(OptTuple<i128>, fn(&str) -> Option<i128>),
+    Integer(OptTuple<i128>),
     Text(OptTuple<&'static str>),
     List(&'static [&'static str]),
     Enum(OptTuple<StrEnum<'static>>),
@@ -31,7 +32,7 @@ defaults! {
     visiblepw                 = false
 
     passwd_tries              = 3
-    umask                     = 0o22 (!= 0o777)    [0..=0o777; radix: 8]
+    umask                     = 0o22 (!= 0o777)
 
     editor                    = "/usr/bin/editor"
     lecture_file              = ""
@@ -81,8 +82,8 @@ mod test {
         test! { match_group_by_gid => Flag(false) };
         test! { use_pty => Flag(false) };
         test! { visiblepw => Flag(false) };
-        test! { passwd_tries => Integer(OptTuple { default: 3, negated: None }, _) };
-        test! { umask => Integer(OptTuple { default: 18, negated: Some(511) }, _) };
+        test! { passwd_tries => Integer(OptTuple { default: 3, negated: None }) };
+        test! { umask => Integer(OptTuple { default: 18, negated: Some(511) }) };
         test! { editor => Text(OptTuple { default: "/usr/bin/editor", negated: None }) };
         test! { lecture_file => Text(_) };
         test! { secure_path => Text(OptTuple { default: "", negated: Some("") }) };
diff --git a/lib/sudo-defaults/src/settings_dsl.rs b/lib/sudo-defaults/src/settings_dsl.rs
index fb50e19c..6f764f00 100644
--- a/lib/sudo-defaults/src/settings_dsl.rs
+++ b/lib/sudo-defaults/src/settings_dsl.rs
@@ -7,13 +7,13 @@ macro_rules! add_from {
         }
     };
 
-    ($ctor:ident, $type:ty, negatable$(, $vetting_function:expr)?) => {
+    ($ctor:ident, $type:ty, negatable) => {
         impl From<$type> for $crate::SudoDefault {
             fn from(value: $type) -> Self {
                 $crate::SudoDefault::$ctor(OptTuple {
                     default: value,
                     negated: None,
-                }$(, $vetting_function)?)
+                })
             }
         }
 
@@ -22,7 +22,7 @@ macro_rules! add_from {
                 $crate::SudoDefault::$ctor(OptTuple {
                     default: value,
                     negated: Some(neg),
-                }$(, $vetting_function)?)
+                })
             }
         }
     };
@@ -56,19 +56,14 @@ macro_rules! optional {
 }
 
 macro_rules! defaults {
-    ($($name:ident = $value:tt $((!= $negate:tt))? $([$($key:ident),*])? $([$first:literal ..= $last:literal$(; radix: $radix: expr)?])?)*) => {
+    ($($name:ident = $value:tt $((!= $negate:tt))? $([$($key:ident),*])?)*) => {
         pub const ALL_PARAMS: &'static [&'static str] = &[
             $(stringify!($name)),*
         ];
 
-        // because of the nature of radix and ranges, 'let mut result' is not always necessary, and
-        // a radix of 10 can also not always be avoided (and for uniformity, I would also not avoid this
-        // if this was hand-written code.
-        #[allow(unused_mut)]
-        #[allow(clippy::from_str_radix_10)]
         pub fn sudo_default(var: &str) -> Option<SudoDefault> {
             add_from!(Flag, bool);
-            add_from!(Integer, i128, negatable, |text| i128::from_str_radix(text, 10).ok());
+            add_from!(Integer, i128, negatable);
             add_from!(Text, &'static str, negatable);
             add_from!(List, &'static [&'static str]);
             add_from!(Enum, StrEnum<'static>, negatable);
@@ -81,13 +76,7 @@ macro_rules! defaults {
                               |key: &'static str| StrEnum::new(key, keys).unwrap_or_else(|| unreachable!())
                           })?];
                           let datum = restrict(sliceify!($value));
-                          let mut result = tupleify!(datum$(, restrict($negate))?).into();
-                          $(
-                              if let SudoDefault::Integer(_, ref mut checker) = &mut result {
-                                  *checker = |text| i128::from_str_radix(text, 10$(*0 + $radix)?).ok().filter(|val| ($first ..= $last).contains(val));
-                              }
-                          )?
-                          result
+                          tupleify!(datum$(, restrict($negate))?).into()
                     },
                     )*
                     _ => return None
diff --git a/lib/sudo-env/Cargo.toml b/lib/sudo-env/Cargo.toml
index d29e26c7..82122cb8 100644
--- a/lib/sudo-env/Cargo.toml
+++ b/lib/sudo-env/Cargo.toml
@@ -11,7 +11,7 @@ build = "build.rs"
 [dependencies]
 sudo-system.workspace = true
 sudo-common.workspace = true
-sudoers.workspace = true
 
 [dev-dependencies]
 sudo-cli.workspace = true
+sudoers.workspace = true
\ No newline at end of file
diff --git a/lib/sudo-env/src/environment.rs b/lib/sudo-env/src/environment.rs
index 652d0f8b..53679390 100644
--- a/lib/sudo-env/src/environment.rs
+++ b/lib/sudo-env/src/environment.rs
@@ -1,8 +1,7 @@
 use std::collections::HashSet;
 
-use sudo_common::context::{CommandAndArguments, Context, Environment};
+use sudo_common::context::{CommandAndArguments, Configuration, Context, Environment};
 use sudo_system::PATH_MAX;
-use sudoers::Policy;
 
 use crate::wildcard_match::wildcard_match;
 
@@ -106,7 +105,7 @@ fn in_table(needle: &str, haystack: &HashSet<String>) -> bool {
 }
 
 /// Determine whether a specific environment variable should be kept
-fn should_keep(key: &str, value: &str, cfg: &impl Policy) -> bool {
+fn should_keep(key: &str, value: &str, cfg: &impl Configuration) -> bool {
     if value.starts_with("()") {
         return false;
     }
@@ -138,7 +137,7 @@ fn should_keep(key: &str, value: &str, cfg: &impl Policy) -> bool {
 pub fn get_target_environment(
     current_env: Environment,
     context: &Context,
-    settings: &impl Policy,
+    settings: &impl Configuration,
 ) -> Environment {
     let mut environment = current_env
         .into_iter()
@@ -153,7 +152,7 @@ pub fn get_target_environment(
 #[cfg(test)]
 mod tests {
     use std::collections::HashSet;
-    use sudoers::Policy;
+    use sudo_common::context::Configuration;
 
     use crate::environment::{is_safe_tz, should_keep, PATH_ZONEINFO};
 
@@ -162,7 +161,7 @@ mod tests {
         check: HashSet<String>,
     }
 
-    impl Policy for TestConfiguration {
+    impl Configuration for TestConfiguration {
         fn env_keep(&self) -> &HashSet<String> {
             &self.keep
         }
diff --git a/lib/sudo-exec/src/lib.rs b/lib/sudo-exec/src/lib.rs
index 4f173afe..3363774b 100644
--- a/lib/sudo-exec/src/lib.rs
+++ b/lib/sudo-exec/src/lib.rs
@@ -31,12 +31,7 @@ pub fn run_command(ctx: Context<'_>, env: Environment) -> io::Result<ExitStatus>
     // reset env and set filtered environment
     command.args(ctx.command.arguments).env_clear().envs(env);
     // set target user and groups
-    set_target_user(
-        &mut command,
-        ctx.current_user,
-        ctx.target_user,
-        ctx.target_group,
-    );
+    set_target_user(&mut command, ctx.target_user);
     // spawn and exec to command
     let mut child = command.spawn()?;
 
diff --git a/lib/sudo-pam/src/converse.rs b/lib/sudo-pam/src/converse.rs
index 9a3e380f..c8c5ddae 100644
--- a/lib/sudo-pam/src/converse.rs
+++ b/lib/sudo-pam/src/converse.rs
@@ -6,6 +6,7 @@ use crate::{error::PamResult, PamErrorType};
 
 /// Each message in a PAM conversation will have a message style. Each of these
 /// styles must be handled separately.
+#[derive(Clone, Copy)]
 pub enum PamMessageStyle {
     /// Prompt for input using a message. The input should considered secret
     /// and should be hidden from view.
@@ -166,17 +167,6 @@ pub(crate) struct ConverserData<C> {
     pub(crate) panicked: bool,
 }
 
-impl<C: Converser> ConverserData<C> {
-    /// This function creates a pam_conv struct with the converse function for
-    /// the specific converser.
-    pub(crate) unsafe fn create_pam_conv(self: std::pin::Pin<&mut Self>) -> pam_conv {
-        pam_conv {
-            conv: Some(converse::<C>),
-            appdata_ptr: self.get_unchecked_mut() as *mut ConverserData<C> as *mut libc::c_void,
-        }
-    }
-}
-
 /// This function implements the conversation function of `pam_conv`.
 ///
 /// This function should always be called with an appdata_ptr that implements
@@ -203,7 +193,7 @@ pub(crate) extern "C" fn converse<C: Converser>(
             messages: Vec::with_capacity(num_msg as usize),
         };
         for i in 0..num_msg as isize {
-            let message: &pam_message = unsafe { &*((*msg).offset(i)) };
+            let message: &pam_message = unsafe { &**msg.offset(i) };
 
             let msg = unsafe { sudo_cutils::string_from_ptr(message.msg) };
             let style = if let Some(style) = PamMessageStyle::from_int(message.msg_style) {
@@ -273,3 +263,167 @@ pub(crate) extern "C" fn converse<C: Converser>(
     };
     res.as_int()
 }
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use std::pin::Pin;
+    use PamMessageStyle::*;
+
+    impl SequentialConverser for String {
+        fn handle_normal_prompt(&self, msg: &str) -> PamResult<String> {
+            Ok(format!("{self} says {msg}"))
+        }
+
+        fn handle_hidden_prompt(&self, msg: &str) -> PamResult<String> {
+            Ok(format!("{self}s secret is {msg}"))
+        }
+
+        fn handle_error(&self, msg: &str) -> PamResult<()> {
+            panic!("{msg}")
+        }
+
+        fn handle_info(&self, _msg: &str) -> PamResult<()> {
+            Ok(())
+        }
+    }
+
+    // essentially do the inverse of the "conversation function"
+    fn dummy_pam(msgs: &[PamMessage], talkie: &pam_conv) -> Vec<Option<String>> {
+        let pam_msgs = msgs
+            .iter()
+            .map(|PamMessage { msg, style, .. }| pam_message {
+                msg: unsafe { sudo_cutils::into_leaky_cstring(msg) },
+                msg_style: *style as i32,
+            })
+            .rev()
+            .collect::<Vec<pam_message>>();
+        let mut ptrs = pam_msgs
+            .iter()
+            .map(|x| x as *const pam_message)
+            .rev()
+            .collect::<Vec<*const pam_message>>();
+
+        let mut raw_response = std::ptr::null_mut::<pam_response>();
+        let conv_err = unsafe {
+            talkie.conv.expect("non-null fn ptr")(
+                ptrs.len() as i32,
+                ptrs.as_mut_ptr(),
+                &mut raw_response,
+                talkie.appdata_ptr,
+            )
+        };
+
+        // deallocate the leaky strings
+        for rec in ptrs {
+            unsafe { libc::free((*rec).msg as *mut _) }
+        }
+        if conv_err != 0 {
+            return vec![];
+        }
+
+        let result = msgs
+            .iter()
+            .enumerate()
+            .map(|(i, _)| unsafe {
+                let ptr = raw_response.add(i);
+                if (*ptr).resp.is_null() {
+                    None
+                } else {
+                    // "The resp_retcode member of this struct is unused and should be set to zero."
+                    assert_eq!((*ptr).resp_retcode, 0);
+                    let response = sudo_cutils::string_from_ptr((*ptr).resp);
+                    libc::free((*ptr).resp as *mut _);
+                    Some(response)
+                }
+            })
+            .collect();
+
+        unsafe { libc::free(raw_response as *mut _) };
+        result
+    }
+
+    fn msg(style: PamMessageStyle, msg: &str) -> PamMessage {
+        let msg = msg.to_string();
+        PamMessage {
+            style,
+            msg,
+            response: None,
+        }
+    }
+
+    // sanity check on the test cases; lib.rs is expected to manage the lifetime of the pointer
+    // inside the pam_conv object explicitly.
+
+    use std::marker::PhantomData;
+    struct PamConvBorrow<'a> {
+        pam_conv: pam_conv,
+        _marker: std::marker::PhantomData<&'a ()>,
+    }
+
+    impl<'a> PamConvBorrow<'a> {
+        fn new<C: Converser>(data: Pin<&'a mut ConverserData<C>>) -> PamConvBorrow<'a> {
+            let appdata_ptr =
+                unsafe { data.get_unchecked_mut() as *mut ConverserData<C> as *mut libc::c_void };
+            PamConvBorrow {
+                pam_conv: pam_conv {
+                    conv: Some(converse::<C>),
+                    appdata_ptr,
+                },
+                _marker: PhantomData,
+            }
+        }
+
+        fn borrow(&self) -> &pam_conv {
+            &self.pam_conv
+        }
+    }
+
+    #[test]
+    fn pam_gpt() {
+        let mut hello = Box::pin(ConverserData {
+            converser: "tux".to_string(),
+            panicked: false,
+        });
+        let cookie = PamConvBorrow::new(hello.as_mut());
+        let pam_conv = cookie.borrow();
+
+        assert_eq!(dummy_pam(&[], pam_conv), vec![]);
+
+        assert_eq!(
+            dummy_pam(&[msg(PromptEchoOn, "hello")], pam_conv),
+            vec![Some("tux says hello".to_string())]
+        );
+
+        assert_eq!(
+            dummy_pam(&[msg(PromptEchoOff, "fish")], pam_conv),
+            vec![Some("tuxs secret is fish".to_string())]
+        );
+
+        assert_eq!(dummy_pam(&[msg(TextInfo, "mars")], pam_conv), vec![None]);
+
+        assert_eq!(
+            dummy_pam(
+                &[
+                    msg(PromptEchoOff, "banging the rocks together"),
+                    msg(TextInfo, ""),
+                    msg(PromptEchoOn, ""),
+                ],
+                pam_conv
+            ),
+            vec![
+                Some("tuxs secret is banging the rocks together".to_string()),
+                None,
+                Some("tux says ".to_string()),
+            ]
+        );
+
+        //assert!(!hello.panicked); // not allowed by borrow checker
+        let real_hello = unsafe { &mut *(pam_conv.appdata_ptr as *mut ConverserData<String>) };
+        assert!(!real_hello.panicked);
+
+        assert_eq!(dummy_pam(&[msg(ErrorMessage, "oops")], pam_conv), vec![]);
+
+        assert!(hello.panicked); // allowed now
+    }
+}
diff --git a/lib/sudo-pam/src/error.rs b/lib/sudo-pam/src/error.rs
index e509972c..4b3b8be9 100644
--- a/lib/sudo-pam/src/error.rs
+++ b/lib/sudo-pam/src/error.rs
@@ -180,3 +180,17 @@ pub(crate) fn pam_err(err: libc::c_int, handle: *const pam_handle_t) -> Result<(
         Err(PamError::from_pam(err, handle))
     }
 }
+
+#[cfg(test)]
+mod test {
+    use super::PamErrorType;
+
+    #[test]
+    fn isomorphy() {
+        for i in -100..100 {
+            let pam = PamErrorType::from_int(i);
+            assert_eq!(pam.as_int(), i);
+            assert_eq!(PamErrorType::from_int(pam.as_int()), pam);
+        }
+    }
+}
diff --git a/lib/sudo-pam/src/lib.rs b/lib/sudo-pam/src/lib.rs
index e7b29bfa..4ea0f74d 100644
--- a/lib/sudo-pam/src/lib.rs
+++ b/lib/sudo-pam/src/lib.rs
@@ -1,6 +1,5 @@
 use std::{
     ffi::{CStr, CString},
-    pin::Pin,
     time::Duration,
 };
 
@@ -16,8 +15,7 @@ mod error;
 pub use converse::CLIConverser;
 
 pub struct PamContext<'a, C: Converser> {
-    data: Pin<Box<ConverserData<C>>>,
-    pam_conv: Option<pam_conv>,
+    data_ptr: *mut ConverserData<C>,
     pamh: Option<&'a mut pam_handle_t>,
     silent: bool,
     allow_null_auth_token: bool,
@@ -66,28 +64,30 @@ impl<C: Converser> PamContextBuilder<C> {
                 None => std::ptr::null(),
             };
 
-            let data = ConverserData {
+            // this will be de-allocated explicitly in this type's drop method
+            let data_ptr = Box::into_raw(Box::new(ConverserData {
                 converser,
                 panicked: false,
-            };
+            }));
 
             let mut context = PamContext {
-                data: Box::pin(data),
-                pam_conv: None,
+                data_ptr,
                 pamh: None,
                 silent: false,
                 allow_null_auth_token: true,
                 last_pam_status: None,
                 session_started: false,
             };
-            context.pam_conv = Some(unsafe { context.data.as_mut().create_pam_conv() });
 
             let mut pamh = std::ptr::null_mut();
             let res = unsafe {
                 pam_start(
                     c_service_name.as_ptr(),
                     c_user_ptr,
-                    &context.pam_conv.unwrap(),
+                    &pam_conv {
+                        conv: Some(converse::converse::<C>),
+                        appdata_ptr: data_ptr as *mut libc::c_void,
+                    },
                     &mut pamh,
                 )
             };
@@ -452,6 +452,11 @@ impl<'a, C: Converser> PamContext<'a, C> {
 
         Ok(res)
     }
+
+    /// Check if anything panicked since the last call.
+    pub fn has_panicked(&self) -> bool {
+        unsafe { (*self.data_ptr).panicked }
+    }
 }
 
 impl<'a> PamContext<'a, CLIConverser> {
@@ -463,6 +468,8 @@ impl<'a> PamContext<'a, CLIConverser> {
 
 impl<'a, C: Converser> Drop for PamContext<'a, C> {
     fn drop(&mut self) {
+        // data_ptr's pointee is de-allocated in this scope
+        let _data = unsafe { Box::from_raw(self.data_ptr) };
         if self.pamh.is_some() {
             if !self.session_started {
                 let _ = self.close_session();
@@ -479,6 +486,5 @@ impl<'a, C: Converser> Drop for PamContext<'a, C> {
                 ) | PAM_DATA_SILENT as i32
             };
         }
-        self.pam_conv = None;
     }
 }
diff --git a/lib/sudo-system/src/lib.rs b/lib/sudo-system/src/lib.rs
index 40f745dd..7db28dc0 100644
--- a/lib/sudo-system/src/lib.rs
+++ b/lib/sudo-system/src/lib.rs
@@ -26,44 +26,12 @@ pub fn hostname() -> String {
 }
 
 /// set target user and groups (uid, gid, additional groups) for a command
-pub fn set_target_user(
-    cmd: &mut std::process::Command,
-    current_user: User,
-    target_user: User,
-    target_group: Group,
-) {
+pub fn set_target_user(cmd: &mut std::process::Command, target_user: User) {
     use std::os::unix::process::CommandExt;
 
-    // means that we are using the default user because `-u` was not passed.
-    let user_is_default = target_user.is_default;
-    // means that we are using the principal gid of the target user because `-g` was not passed or
-    // was passed with the principal gid.
-    let group_is_default = target_user.uid == target_group.gid;
-
-    let (uid, gid, groups) = if group_is_default {
-        // no `-g`: We just set the uid, gid and groups using the target user.
-        (
-            target_user.uid,
-            target_user.gid,
-            target_user.groups.unwrap_or_default(),
-        )
-    } else if user_is_default {
-        //  `-g` and no `-u`: The set uid must be the one of the current user and the set groups
-        //  must be the ones of the current user extended with the target group gid.
-        let mut groups = current_user.groups.unwrap_or_default();
-        if !groups.contains(&target_group.gid) {
-            groups.push(target_group.gid);
-        }
-        (current_user.uid, target_group.gid, groups)
-    } else {
-        // `-g` and `-u`: The set uid must be the one of the target user and the set groups must be
-        // the ones of the target group extended with the target group gid.
-        let mut groups = target_user.groups.unwrap_or_default();
-        if !groups.contains(&target_group.gid) {
-            groups.push(target_group.gid);
-        }
-        (target_user.uid, target_group.gid, groups)
-    };
+    let uid = target_user.uid;
+    let gid = target_user.gid;
+    let groups = target_user.groups.unwrap_or_default();
 
     // we need to do this in a `pre_exec` call since the `groups` method in `process::Command` is unstable
     // see https://github.com/rust-lang/rust/blob/a01b4cc9f375f1b95fa8195daeea938d3d9c4c34/library/std/src/sys/unix/process/process_unix.rs#L329-L352
@@ -102,7 +70,6 @@ pub struct User {
     pub shell: String,
     pub passwd: String,
     pub groups: Option<Vec<libc::gid_t>>,
-    pub is_default: bool,
 }
 
 impl User {
@@ -116,7 +83,6 @@ impl User {
             shell: unsafe { string_from_ptr(pwd.pw_shell) },
             passwd: unsafe { string_from_ptr(pwd.pw_passwd) },
             groups: None,
-            is_default: false,
         }
     }
 
diff --git a/lib/sudoers/src/ast.rs b/lib/sudoers/src/ast.rs
index afc7ea14..1f56b4a4 100644
--- a/lib/sudoers/src/ast.rs
+++ b/lib/sudoers/src/ast.rs
@@ -305,22 +305,14 @@ impl Parse for ProtoCommandSpec {
             }
         }
 
-        let start_pos = stream.get_pos();
         let digest = if let Some(Username(keyword)) = try_nonterminal(stream)? {
             let hash_type = match keyword.as_str() {
                 "sha224" => 224,
                 "sha256" => 256,
                 "sha384" => 384,
                 "sha512" => 512,
-                "sudoedit" => {
-                    // note: special behaviour of forward slashes in wildcards, tread carefully
-                    unrecoverable!(stream, "sudoedit is not yet supported");
-                }
-                _ => unrecoverable!(
-                    pos = start_pos,
-                    stream,
-                    "expected command but found {keyword}"
-                ),
+                "sudoedit" => todo!(), // note: special behaviour of forward slashes in wildcards, tread carefully
+                _ => unrecoverable!(stream, "expected command but found {keyword}"),
             };
             expect_syntax(':', stream)?;
             let hex = expect_nonterminal::<Sha2>(stream)?;
@@ -472,24 +464,18 @@ fn parse_include(stream: &mut impl CharStream) -> Parsed<Sudo> {
             expect_syntax('"', stream)?;
             make(path)
         } else {
-            let value_pos = stream.get_pos();
             let IncludePath(path) = expect_nonterminal(stream)?;
             if stream.peek() != Some('\n') {
-                unrecoverable!(
-                    pos = value_pos,
-                    stream,
-                    "use quotes around filenames or escape whitespace"
-                )
+                unrecoverable!(stream, "use quotes around filenames or escape whitespace")
             }
             make(path)
         }
     }
 
-    let key_pos = stream.get_pos();
     let result = match try_nonterminal(stream)? {
         Some(Username(key)) if key == "include" => Sudo::Include(get_path(stream)?),
         Some(Username(key)) if key == "includedir" => Sudo::IncludeDir(get_path(stream)?),
-        _ => unrecoverable!(pos = key_pos, stream, "unknown directive"),
+        _ => unrecoverable!(stream, "unknown directive"),
     };
 
     make(result)
@@ -551,7 +537,7 @@ fn get_directive(
     fn parse_default<T: CharStream>(stream: &mut T) -> Parsed<Directive> {
         let id_pos = stream.get_pos();
 
-        let list_items = |mode: Mode, name: String, cfg: Setting, stream: &mut T| {
+        let list_items = |mode: Mode, name: String, cfg: Setting, stream: &mut _| {
             expect_syntax('=', stream)?;
             if !matches!(cfg, Setting::List(_)) {
                 unrecoverable!(pos = id_pos, stream, "{name} is not a list parameter");
@@ -575,7 +561,6 @@ fn get_directive(
         use sudo_defaults::OptTuple;
 
         if is_syntax('!', stream)? {
-            let value_pos = stream.get_pos();
             let EnvVar(name) = expect_nonterminal(stream)?;
             let value = match sudo_default(&name) {
                 Some(Setting::Flag(_)) => ConfigValue::Flag(false),
@@ -586,23 +571,16 @@ fn get_directive(
                 Some(Setting::Enum(OptTuple {
                     negated: Some(val), ..
                 })) => ConfigValue::Enum(val),
-                Some(Setting::Integer(
-                    OptTuple {
-                        negated: Some(val), ..
-                    },
-                    _checker,
-                )) => ConfigValue::Num(val),
-                _ => unrecoverable!(
-                    pos = value_pos,
-                    stream,
-                    "`{name}' cannot be used in a boolean context"
-                ),
+                Some(Setting::Integer(OptTuple {
+                    negated: Some(val), ..
+                })) => ConfigValue::Num(val),
+                _ => unrecoverable!(stream, "`{name}' cannot be used in a boolean context"),
             };
             make(Defaults(name, value))
         } else {
             let EnvVar(name) = try_nonterminal(stream)?;
             let Some(cfg) = sudo_default(&name) else {
-                unrecoverable!(pos = id_pos, stream, "unknown setting: `{name}'");
+                unrecoverable!(pos=id_pos, stream, "unknown setting: `{name}'");
             };
 
             if is_syntax('+', stream)? {
@@ -610,23 +588,11 @@ fn get_directive(
             } else if is_syntax('-', stream)? {
                 list_items(Mode::Del, name, cfg, stream)
             } else if is_syntax('=', stream)? {
-                let value_pos = stream.get_pos();
                 match cfg {
                     Setting::Flag(_) => {
                         unrecoverable!(stream, "can't assign to boolean setting `{name}'")
                     }
-                    Setting::Integer(_, checker) => {
-                        let Numeric(denotation) = expect_nonterminal(stream)?;
-                        if let Some(value) = checker(&denotation) {
-                            make(Defaults(name, ConfigValue::Num(value)))
-                        } else {
-                            unrecoverable!(
-                                pos = value_pos,
-                                stream,
-                                "`{denotation}' is not a valid value for {name}"
-                            );
-                        }
-                    }
+                    Setting::Integer(_) => todo!(),
                     Setting::List(_) => {
                         let items = parse_vars(stream)?;
                         make(Defaults(name, ConfigValue::List(Mode::Set, items)))
@@ -636,13 +602,10 @@ fn get_directive(
                         make(Defaults(name, ConfigValue::Text(text)))
                     }
                     Setting::Enum(sudo_defaults::OptTuple { default: key, .. }) => {
+                        let value_pos = stream.get_pos();
                         let text = text_item(stream)?;
                         let Some(value) = key.alt(&text) else {
-                            unrecoverable!(
-                                pos = value_pos,
-                                stream,
-                                "`{text}' is not a valid value for {name}"
-                            );
+                            unrecoverable!(pos = value_pos, stream, "`{text}' is not a valid value for {name}");
                         };
                         make(Defaults(name, ConfigValue::Enum(value)))
                     }
diff --git a/lib/sudoers/src/ast_names.rs b/lib/sudoers/src/ast_names.rs
index 6fd417a6..8935e19d 100644
--- a/lib/sudoers/src/ast_names.rs
+++ b/lib/sudoers/src/ast_names.rs
@@ -15,7 +15,7 @@ mod names {
         const DESCRIPTION: &'static str = "number";
     }
 
-    impl UserFriendly for tokens::Numeric {
+    impl UserFriendly for tokens::Decimal {
         const DESCRIPTION: &'static str = "number";
     }
 
diff --git a/lib/sudoers/src/basic_parser.rs b/lib/sudoers/src/basic_parser.rs
index 5c24a42d..eb9715c8 100644
--- a/lib/sudoers/src/basic_parser.rs
+++ b/lib/sudoers/src/basic_parser.rs
@@ -28,7 +28,7 @@
 /// Type holding a parsed object (or error information if parsing failed)
 pub type Parsed<T> = Result<T, Status>;
 
-pub type Position = std::ops::Range<(usize, usize)>;
+pub type Position = (usize, usize);
 
 #[derive(Debug, Clone)]
 #[cfg_attr(test, derive(PartialEq))]
@@ -46,17 +46,15 @@ pub fn reject<T>() -> Parsed<T> {
 }
 
 macro_rules! unrecoverable {
+    // TODO: transmit begin-and-end position information to give an accurate "underlining" of the full error
     (pos=$pos:expr, $stream:ident, $($str:expr),*) => {
-        return Err(crate::basic_parser::Status::Fatal($pos .. $stream.get_pos(), format![$($str),*]))
+        return Err(crate::basic_parser::Status::Fatal($pos,format![$($str),*]))
     };
     ($stream:ident, $($str:expr),*) => {
-        {
-            let pos = $stream.get_pos();
-            return Err(crate::basic_parser::Status::Fatal(pos .. pos, format![$($str),*]))
-        }
+        return Err(crate::basic_parser::Status::Fatal($stream.get_pos(),format![$($str),*]))
     };
     ($($str:expr),*) => {
-        return Err(crate::basic_parser::Status::Fatal(Default::default(), format![$($str),*]))
+        return Err(crate::basic_parser::Status::Fatal(None,format![$($str),*]))
     };
 }
 
@@ -204,10 +202,9 @@ pub fn try_nonterminal<T: Parse>(stream: &mut impl CharStream) -> Parsed<T> {
 use crate::ast_names::UserFriendly;
 
 pub fn expect_nonterminal<T: Parse + UserFriendly>(stream: &mut impl CharStream) -> Parsed<T> {
-    let begin_pos = stream.get_pos();
     match try_nonterminal(stream) {
         Err(Status::Reject) => {
-            unrecoverable!(pos = begin_pos, stream, "expected {}", T::DESCRIPTION)
+            unrecoverable!(stream, "expected {}", T::DESCRIPTION)
         }
         result => result,
     }
diff --git a/lib/sudoers/src/lib.rs b/lib/sudoers/src/lib.rs
index 7fa2e3c5..cf92454c 100644
--- a/lib/sudoers/src/lib.rs
+++ b/lib/sudoers/src/lib.rs
@@ -37,14 +37,33 @@ pub struct Request<'a, User: UnixUser, Group: UnixGroup> {
     pub arguments: &'a str,
 }
 
-#[derive(Debug, Default)]
-pub struct Judgement {
+/// Data types that represent what the "terms and conditions" are
+/// (this is currently a stub and should later be turned into a trait and moved to a sudo-policies crate)
+#[derive(Debug)]
+pub struct Policy {
     flags: Option<Tag>,
-    settings: Settings,
+    pub settings: Settings, // TODO: hide behind interface
+}
+
+pub enum Authorization {
+    Required,
+    Passed,
+    Forbidden,
 }
 
-mod policy;
-pub use policy::{Authorization, Policy};
+impl Policy {
+    pub fn authorization(&self) -> Authorization {
+        if let Some(tag) = &self.flags {
+            if !tag.passwd {
+                Authorization::Passed
+            } else {
+                Authorization::Required
+            }
+        } else {
+            Authorization::Forbidden
+        }
+    }
+}
 
 /// This function takes a file argument for a sudoers file and processes it.
 impl Sudoers {
@@ -58,7 +77,7 @@ impl Sudoers {
         am_user: &User,
         on_host: &str,
         request: Request<User, Group>,
-    ) -> Judgement {
+    ) -> Policy {
         // exception: if user is root or does not switch users, NOPASSWD is implied
         let skip_passwd =
             am_user.is_root() || (request.user == am_user && in_group(am_user, request.group));
@@ -70,7 +89,7 @@ impl Sudoers {
             }
         }
 
-        Judgement {
+        Policy {
             flags,
             settings: self.settings.clone(), // this is wasteful, but in the future this will not be a simple clone and it avoids a lifetime
         }
@@ -292,7 +311,7 @@ impl Default for Settings {
                 SudoDefault::Enum(OptTuple { default, .. }) => {
                     this.enum_value.insert(key.to_string(), default);
                 }
-                SudoDefault::Integer(OptTuple { default, .. }, _) => {
+                SudoDefault::Integer(OptTuple { default, .. }) => {
                     this.int_value.insert(key.to_string(), default);
                 }
                 SudoDefault::List(default) => {
diff --git a/lib/sudoers/src/policy.rs b/lib/sudoers/src/policy.rs
deleted file mode 100644
index edcfc236..00000000
--- a/lib/sudoers/src/policy.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use super::Judgement;
-/// Data types and traits that represent what the "terms and conditions" are after a succesful
-/// permission check.
-///
-/// The trait definitions can be part of some global crate in the future, if we support more
-/// than just the sudoers file.
-use std::collections::HashSet;
-
-pub trait Policy {
-    fn env_keep(&self) -> &HashSet<String>;
-    fn env_check(&self) -> &HashSet<String>;
-    fn authorization(&self) -> Authorization {
-        Authorization::Forbidden
-    }
-}
-
-pub enum Authorization {
-    Required,
-    Passed,
-    Forbidden,
-}
-
-impl Policy for Judgement {
-    fn authorization(&self) -> Authorization {
-        if let Some(tag) = &self.flags {
-            if !tag.passwd {
-                Authorization::Passed
-            } else {
-                Authorization::Required
-            }
-        } else {
-            Authorization::Forbidden
-        }
-    }
-
-    fn env_keep(&self) -> &HashSet<String> {
-        self.settings
-            .list
-            .get("env_keep")
-            .expect("env_keep missing from settings")
-    }
-
-    fn env_check(&self) -> &HashSet<String> {
-        self.settings
-            .list
-            .get("env_check")
-            .expect("env_check missing from settings")
-    }
-}
diff --git a/lib/sudoers/src/tokens.rs b/lib/sudoers/src/tokens.rs
index 2682882b..1bb9e07e 100644
--- a/lib/sudoers/src/tokens.rs
+++ b/lib/sudoers/src/tokens.rs
@@ -39,17 +39,19 @@ impl Token for Digits {
 }
 
 #[derive(Debug)]
-pub struct Numeric(pub String);
-
-impl Token for Numeric {
-    const MAX_LEN: usize = 38;
+pub struct Decimal(pub i32);
 
+impl Token for Decimal {
     fn construct(s: String) -> Result<Self, String> {
-        Ok(Numeric(s))
+        Ok(Decimal(s.parse().unwrap()))
     }
 
     fn accept(c: char) -> bool {
-        c.is_ascii_hexdigit()
+        c.is_ascii_digit()
+    }
+
+    fn accept_1st(c: char) -> bool {
+        c.is_ascii_digit() || "+-".contains(c)
     }
 }
 
diff --git a/sudo/src/diagnostic.rs b/sudo/src/diagnostic.rs
deleted file mode 100644
index adb99cc6..00000000
--- a/sudo/src/diagnostic.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use std::fs::File;
-use std::io::{BufRead, BufReader};
-use std::ops::Range;
-use std::path::Path;
-
-pub(crate) fn cited_error(message: &str, range: Range<(usize, usize)>, path: impl AsRef<Path>) {
-    let path_str = path.as_ref().display();
-    let Range {
-        start: (line, col),
-        end: (end_line, mut end_col),
-    } = range;
-    eprintln!("{path_str}:{line}:{col}: {message}");
-
-    // we won't try to "span" errors across multiple lines
-    if line != end_line {
-        end_col = col;
-    }
-
-    let citation = || {
-        let inp = BufReader::new(File::open(path).ok()?);
-        let line = inp.lines().nth(line - 1)?.ok()?;
-        let padding = line
-            .chars()
-            .take(col - 1)
-            .map(|c| if c.is_whitespace() { c } else { ' ' })
-            .collect::<String>();
-        let lineunder = std::iter::repeat('~')
-            .take(end_col - col)
-            .skip(1)
-            .collect::<String>();
-        eprintln!("{line}");
-        eprintln!("{padding}^{lineunder}");
-        Some(())
-    };
-
-    // we ignore any errors in displaying an error
-    let _ = citation();
-}
-
-macro_rules! diagnostic {
-    ($str:expr, $path:tt @ $pos:ident) => {
-        if let Some(range) = $pos {
-            $crate::diagnostic::cited_error(&format!($str), range, $path);
-        } else {
-            eprintln!($str);
-        }
-    };
-}
-
-pub(crate) use diagnostic;
diff --git a/sudo/src/main.rs b/sudo/src/main.rs
index fbab88b7..c6c2a054 100644
--- a/sudo/src/main.rs
+++ b/sudo/src/main.rs
@@ -7,9 +7,8 @@ use sudo_common::{
     error::Error,
 };
 use sudo_env::environment;
-use sudoers::{Authorization, Policy, Sudoers};
+use sudoers::{Authorization, Sudoers};
 
-mod diagnostic;
 mod pam;
 
 fn parse_sudoers() -> Result<Sudoers, Error> {
@@ -19,15 +18,15 @@ fn parse_sudoers() -> Result<Sudoers, Error> {
     let (sudoers, syntax_errors) = Sudoers::new(sudoers_path)
         .map_err(|e| Error::Configuration(format!("no valid sudoers file: {e}")))?;
 
-    for sudoers::Error(pos, error) in syntax_errors {
-        diagnostic::diagnostic!("{error}", sudoers_path @ pos);
+    for sudoers::Error(_pos, error) in syntax_errors {
+        eprintln!("Parse error: {error}");
     }
 
     Ok(sudoers)
 }
 
 /// parse suoers file and check permission to run the provided command given the context
-fn check_sudoers(sudoers: &Sudoers, context: &Context) -> sudoers::Judgement {
+fn check_sudoers(sudoers: &Sudoers, context: &Context) -> sudoers::Policy {
     sudoers.check(
         &context.current_user,
         &context.hostname,
@@ -82,7 +81,7 @@ fn main() -> Result<(), Error> {
         }
     };
 
-    let target_env = environment::get_target_environment(current_env, &context, &policy);
+    let target_env = environment::get_target_environment(current_env, &context, &policy.settings);
 
     // run command and return corresponding exit code
     match sudo_exec::run_command(context, target_env) {


Test patch:
diff --git a/lib/sudo-env/tests/env_tests.rs b/lib/sudo-env/tests/env_tests.rs
index 6a5acef4..a0b57d51 100644
--- a/lib/sudo-env/tests/env_tests.rs
+++ b/lib/sudo-env/tests/env_tests.rs
@@ -3,6 +3,7 @@ use sudo_cli::SudoOptions;
 use sudo_common::context::{CommandAndArguments, Context, Environment};
 use sudo_env::environment::get_target_environment;
 use sudo_system::{Group, User};
+use sudoers::Settings;
 
 const TESTS: &str = "
 > env
@@ -85,7 +86,6 @@ fn create_test_context<'a>(sudo_options: &'a SudoOptions) -> Context<'a> {
         shell: "/bin/sh".to_string(),
         passwd: String::new(),
         groups: None,
-        is_default: false,
     };
 
     let current_group = Group {
@@ -104,7 +104,6 @@ fn create_test_context<'a>(sudo_options: &'a SudoOptions) -> Context<'a> {
         shell: "/bin/bash".to_string(),
         passwd: String::new(),
         groups: None,
-        is_default: false,
     };
 
     let root_group = Group {
@@ -148,7 +147,7 @@ fn test_environment_variable_filtering() {
 
     for (cmd, expected_env) in parts {
         let options = SudoOptions::try_parse_from(cmd.split_whitespace()).unwrap();
-        let settings = sudoers::Judgement::default();
+        let settings = Settings::default();
         let context = create_test_context(&options);
         let resulting_env = get_target_environment(initial_env.clone(), &context, &settings);
 
diff --git a/lib/sudoers/src/test/mod.rs b/lib/sudoers/src/test/mod.rs
index 00f68aac..75aaaa10 100644
--- a/lib/sudoers/src/test/mod.rs
+++ b/lib/sudoers/src/test/mod.rs
@@ -296,8 +296,8 @@ fn default_set_test() {
         "Defaults !env_check",
         "Defaults env_check += \"FOO\"",
         "Defaults env_check += \"XYZZY\"",
-        "Defaults umask = 0123",
-        "Defaults passwd_tries = 5",
+        //"Defaults umask = 0123",
+        //"Defaults passwd_tries = 5",
         "Defaults lecture_file = \"/etc/sudoers\"",
         "Defaults secure_path = /etc"
     ]);
@@ -314,14 +314,8 @@ fn default_set_test() {
     );
     assert_eq!(settings.str_value["lecture_file"], "/etc/sudoers");
     assert_eq!(settings.str_value["secure_path"], "/etc");
-    assert_eq!(settings.int_value["umask"], 0o123);
-    assert_eq!(settings.int_value["passwd_tries"], 5);
-
-    assert!(parse_string::<Sudo>("Defaults umask = 789").is_err());
-    assert!(parse_string::<Sudo>("Defaults umask = 1234").is_err());
-    assert!(parse_string::<Sudo>("Defaults verifypw = \"sometimes\"").is_err());
-    assert!(parse_string::<Sudo>("Defaults verifypw = sometimes").is_err());
-    assert!(parse_string::<Sudo>("Defaults verifypw = never").is_ok());
+    //assert_eq!(settings.int_value["umask"], 0o123);
+    //assert_eq!(settings.int_value["passwd_tries"], 5);
 }
 
 #[test]
diff --git a/test-binaries/checkpermit.rs b/test-binaries/checkpermit.rs
index 5f54f197..5f350787 100644
--- a/test-binaries/checkpermit.rs
+++ b/test-binaries/checkpermit.rs
@@ -128,7 +128,7 @@ fn main() {
     let args: Vec<String> = env::args().collect();
     if let Ok((cfg, warn)) = Sudoers::new("./sudoers") {
         for sudoers::Error(pos, msg) in warn {
-            if let Some(std::ops::Range { start: (x, y), .. }) = pos {
+            if let Some((x, y)) = pos {
                 fancy_error(x, y, "./sudoers");
             }
             eprintln!("{msg}");
diff --git a/test-binaries/sudoers b/test-binaries/sudoers
index d11f7386..0252683e 100644
--- a/test-binaries/sudoers
+++ b/test-binaries/sudoers
@@ -2,7 +2,6 @@ Defaults	env_reset
 Defaults	env_keep = "AAP NOOT MIES"
 Defaults	env_keep += "MIES WIM ZUS JET" * AAP
 Defaults	env_keep -= "AAP NOOT ZUS JET"
-Defaults	umask=778
 
 Defaults	mail_badpass
 Defaults	secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
diff --git a/test-framework/README.md b/test-framework/README.md
index f136f8b2..647c5207 100644
--- a/test-framework/README.md
+++ b/test-framework/README.md
@@ -30,45 +30,3 @@ $ SUDO_UNDER_TEST=ours cargo test -p sudo-compliance-tests
 Tests (`#[test]` functions) that exercise behavior not yet implemented in sudo-rs MUST be marked as `#[ignored]`.
 When said behavior is implemented in sudo-rs, the `#[ignored]` attribute MUST be removed from the test.
 CI will run `#[ignored]` tests against sudo-rs and fail the build if any of them passes -- as that indicates that an `#[ignored]` attribute was not removed.
-
-## Using docker containers as a sudo playground
-
-### Original sudo
-
-*After* you have run `cargo t -p sudo-compliance-tests` you'll be able to spin up a container based on the docker image used to run the tests against the original sudo with the following command:
-
-``` console
-$ docker run --rm -it sudo-test-og
-
-root@5b3a062d6dcc:/#
-```
-
-After you have an active container you can spawn shells as regular users using the following command
-
-``` console
-$ docker exec -u 1000:100 -it 5b3a062d6dcc bash
-
-I have no name!@5b3a062d6dcc:/$
-```
-
-`1000` is the user ID you'll assume and `100` is the group ID. `5b3a062d6dcc` is the ID of the docker container. You can get the ID from either the shell prompt you get after running `docker run` or from `docker ps`.
-
-Note that terminating the `docker run` shell will terminate all the `docker exec` shells. The docker container will be removed after the `docker run` shell is terminated.
-
-### sudo-rs
-
-*After* you have run `SUDO_UNDER_TEST=ours cargo t -p sudo-compliance-tests` a Docker image that has a source build of `sudo-rs` installed as the system sudo (`/usr/src/bin/sudo`) will become available. The docker image is named `sudo-test-rs`.
-
-All the instructions in the previous subsection can be used to test `sudo-rs` within a docker container: simply use `sudo-test-rs` instead of `sudo-test-og` as the docker image name.
-
-``` console
-$ docker run --rm -it sudo-test-rs
-
-root@b5ee5351b9c6:/usr/src/sudo#
-```
-
-``` console
-$ docker exec -u 1000:100 -it b5ee5351b9c6 bash
-
-I have no name!@b5ee5351b9c6:/usr/src/sudo$
-```
diff --git a/test-framework/sudo-compliance-tests/src/child_process.rs b/test-framework/sudo-compliance-tests/src/child_process.rs
index c76cc447..db67dc5d 100644
--- a/test-framework/sudo-compliance-tests/src/child_process.rs
+++ b/test-framework/sudo-compliance-tests/src/child_process.rs
@@ -45,22 +45,3 @@ fn sudo_forwards_childs_stderr() -> Result<()> {
 
     Ok(())
 }
-
-#[test]
-fn sudo_forwards_stdin_to_child() -> Result<()> {
-    let expected = "hello";
-    let path = "/root/file";
-    let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build()?;
-
-    Command::new("sudo")
-        .args(["tee", path])
-        .stdin(expected)
-        .exec(&env)?
-        .assert_success()?;
-
-    let actual = Command::new("cat").arg(path).exec(&env)?.stdout()?;
-
-    assert_eq!(expected, actual);
-
-    Ok(())
-}
diff --git a/test-framework/sudo-compliance-tests/src/flag_group.rs b/test-framework/sudo-compliance-tests/src/flag_group.rs
deleted file mode 100644
index 8b4da155..00000000
--- a/test-framework/sudo-compliance-tests/src/flag_group.rs
+++ /dev/null
@@ -1,123 +0,0 @@
-use std::collections::HashSet;
-
-use pretty_assertions::assert_eq;
-use sudo_test::{Command, Env, Group, User};
-
-use crate::{Result, GROUPNAME, SUDOERS_ALL_ALL_NOPASSWD, USERNAME};
-
-#[test]
-fn changes_the_group_id() -> Result<()> {
-    let expected_gid = 1234;
-    let env = Env(SUDOERS_ALL_ALL_NOPASSWD)
-        .user(USERNAME)
-        .group(Group(GROUPNAME).id(expected_gid))
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        let actual = Command::new("sudo")
-            .args(["-g", GROUPNAME, "id", "-g"])
-            .as_user(user)
-            .exec(&env)?
-            .stdout()?
-            .parse::<u32>()?;
-
-        assert_eq!(expected_gid, actual);
-    }
-
-    Ok(())
-}
-
-#[test]
-fn adds_group_to_groups_output() -> Result<()> {
-    let extra_group = "rustaceans";
-    let env = Env(SUDOERS_ALL_ALL_NOPASSWD)
-        .user(User(USERNAME).secondary_group("secondary-group"))
-        .group(Group(extra_group))
-        .group("secondary-group")
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        let stdout = Command::new("groups").as_user(user).exec(&env)?.stdout()?;
-        let groups_without_sudo = stdout.split_ascii_whitespace().collect::<HashSet<_>>();
-
-        let stdout = Command::new("sudo")
-            .args(["-g", GROUPNAME, "groups"])
-            .as_user(user)
-            .exec(&env)?
-            .stdout()?;
-        let mut groups_with_sudo = stdout.split_ascii_whitespace().collect::<HashSet<_>>();
-
-        assert!(groups_with_sudo.remove(extra_group));
-
-        assert_eq!(groups_with_sudo, groups_without_sudo);
-    }
-
-    Ok(())
-}
-
-#[test]
-fn group_can_be_specified_by_id() -> Result<()> {
-    let expected_gid = 1234;
-    let env = Env(SUDOERS_ALL_ALL_NOPASSWD)
-        .user(USERNAME)
-        .group(Group(GROUPNAME).id(expected_gid))
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        let actual = Command::new("sudo")
-            .arg("-g")
-            .arg(format!("#{expected_gid}"))
-            .args(["id", "-g"])
-            .as_user(user)
-            .exec(&env)?
-            .stdout()?
-            .parse::<u32>()?;
-
-        assert_eq!(expected_gid, actual);
-    }
-
-    Ok(())
-}
-
-#[test]
-#[ignore]
-fn can_use_unassigned_group_id() -> Result<()> {
-    let expected_gid = 1234;
-    let env = Env(SUDOERS_ALL_ALL_NOPASSWD).user(USERNAME).build()?;
-
-    for user in ["root", USERNAME] {
-        let actual = Command::new("sudo")
-            .arg("-g")
-            .arg(format!("#{expected_gid}"))
-            .args(["id", "-g"])
-            .as_user(user)
-            .exec(&env)?
-            .stdout()?
-            .parse::<u32>()?;
-
-        assert_eq!(expected_gid, actual);
-    }
-
-    Ok(())
-}
-
-#[test]
-fn group_does_not_exist() -> Result<()> {
-    let env = Env(SUDOERS_ALL_ALL_NOPASSWD).user(USERNAME).build()?;
-
-    for user in ["root", USERNAME] {
-        let output = Command::new("sudo")
-            .args(["-g", "ghosts", "true"])
-            .as_user(user)
-            .exec(&env)?;
-
-        assert!(!output.status().success());
-        assert_eq!(Some(1), output.status().code());
-
-        if sudo_test::is_original_sudo() {
-            assert_contains!(output.stderr(), "unknown group: ghosts");
-        }
-    }
-
-    Ok(())
-}
diff --git a/test-framework/sudo-compliance-tests/src/flag_user.rs b/test-framework/sudo-compliance-tests/src/flag_user.rs
index 2cdaa981..7cb5eb50 100644
--- a/test-framework/sudo-compliance-tests/src/flag_user.rs
+++ b/test-framework/sudo-compliance-tests/src/flag_user.rs
@@ -1,10 +1,7 @@
 use pretty_assertions::assert_eq;
 use sudo_test::{Command, Env, User};
 
-use crate::{
-    Result, GROUPNAME, SUDOERS_ALL_ALL_NOPASSWD, SUDOERS_ROOT_ALL_NOPASSWD,
-    SUDOERS_USER_ALL_NOPASSWD, USERNAME,
-};
+use crate::{Result, GROUPNAME, SUDOERS_ROOT_ALL_NOPASSWD, SUDOERS_USER_ALL_NOPASSWD, USERNAME};
 
 #[test]
 fn root_can_become_another_user_by_name() -> Result<()> {
@@ -75,7 +72,7 @@ fn invoking_user_groups_are_lost_when_becoming_another_user() -> Result<()> {
     let another_user = "another_user";
     let env = Env(SUDOERS_USER_ALL_NOPASSWD)
         .group(GROUPNAME)
-        .user(User(invoking_user).secondary_group(GROUPNAME))
+        .user(User(invoking_user).group(GROUPNAME))
         .user(another_user)
         .build()?;
 
@@ -93,42 +90,3 @@ fn invoking_user_groups_are_lost_when_becoming_another_user() -> Result<()> {
 
     Ok(())
 }
-
-#[test]
-#[ignore]
-fn can_use_unassigned_user_id() -> Result<()> {
-    let expected_uid = 1234;
-    let env = Env(SUDOERS_ALL_ALL_NOPASSWD).user(USERNAME).build()?;
-
-    for user in ["root", USERNAME] {
-        let actual = Command::new("sudo")
-            .arg("-u")
-            .arg(format!("#{expected_uid}"))
-            .args(["id", "-u"])
-            .as_user(user)
-            .exec(&env)?
-            .stdout()?
-            .parse::<u32>()?;
-
-        assert_eq!(expected_uid, actual);
-    }
-
-    Ok(())
-}
-
-#[test]
-fn user_does_not_exist() -> Result<()> {
-    let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build()?;
-
-    let output = Command::new("sudo")
-        .args(["-u", "ghost", "true"])
-        .exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-    if sudo_test::is_original_sudo() {
-        assert_contains!(output.stderr(), "unknown user: ghost");
-    }
-
-    Ok(())
-}
diff --git a/test-framework/sudo-compliance-tests/src/lib.rs b/test-framework/sudo-compliance-tests/src/lib.rs
index 03e8a13c..1f9e7e6d 100644
--- a/test-framework/sudo-compliance-tests/src/lib.rs
+++ b/test-framework/sudo-compliance-tests/src/lib.rs
@@ -5,7 +5,6 @@ mod macros;
 
 mod child_process;
 mod env_reset;
-mod flag_group;
 mod flag_user;
 mod nopasswd;
 mod pam;
@@ -21,8 +20,5 @@ const GROUPNAME: &str = "rustaceans";
 const PASSWORD: &str = "strong-password";
 
 const SUDOERS_ROOT_ALL: &str = "root    ALL=(ALL:ALL) ALL";
-const SUDOERS_ALL_ALL_NOPASSWD: &str = "ALL ALL=(ALL:ALL) NOPASSWD: ALL";
 const SUDOERS_ROOT_ALL_NOPASSWD: &str = "root ALL=(ALL:ALL) NOPASSWD: ALL";
 const SUDOERS_USER_ALL_NOPASSWD: &str = "ferris ALL=(ALL:ALL) NOPASSWD: ALL";
-
-const PAMD_SUDO_PAM_PERMIT: &str = "auth sufficient pam_permit.so";
diff --git a/test-framework/sudo-compliance-tests/src/nopasswd.rs b/test-framework/sudo-compliance-tests/src/nopasswd.rs
index 464aa71f..9b6fe7d4 100644
--- a/test-framework/sudo-compliance-tests/src/nopasswd.rs
+++ b/test-framework/sudo-compliance-tests/src/nopasswd.rs
@@ -1,8 +1,8 @@
 //! Scenarios where a password does not need to be provided
 
-use sudo_test::{Command, Env, User};
+use sudo_test::{Command, Env};
 
-use crate::{Result, GROUPNAME, SUDOERS_ROOT_ALL, USERNAME};
+use crate::{Result, SUDOERS_ROOT_ALL, USERNAME};
 
 // NOTE all these tests assume that the invoking user passes the sudoers file 'User_List' criteria
 
@@ -33,21 +33,6 @@ fn user_as_themselves() -> Result<()> {
         .assert_success()
 }
 
-#[test]
-#[ignore]
-fn user_as_their_own_group() -> Result<()> {
-    let env = Env(format!("{USERNAME}    ALL=(ALL:ALL) ALL"))
-        .group(GROUPNAME)
-        .user(User(USERNAME).secondary_group(GROUPNAME))
-        .build()?;
-
-    Command::new("sudo")
-        .args(["-g", GROUPNAME, "true"])
-        .as_user(USERNAME)
-        .exec(&env)?
-        .assert_success()
-}
-
 #[test]
 fn nopasswd_tag() -> Result<()> {
     let env = Env(format!("{USERNAME}    ALL=(ALL:ALL) NOPASSWD: ALL"))
diff --git a/test-framework/sudo-compliance-tests/src/perms.rs b/test-framework/sudo-compliance-tests/src/perms.rs
index eab620fc..f8a1d663 100644
--- a/test-framework/sudo-compliance-tests/src/perms.rs
+++ b/test-framework/sudo-compliance-tests/src/perms.rs
@@ -38,6 +38,7 @@ fn user_can_write_file_owned_by_root() -> Result<()> {
 }
 
 #[test]
+#[ignore]
 fn user_can_execute_file_owned_by_root() -> Result<()> {
     let path = "/root/file";
     let env = Env(SUDOERS_USER_ALL_NOPASSWD)
diff --git a/test-framework/sudo-compliance-tests/src/sudoers.rs b/test-framework/sudo-compliance-tests/src/sudoers.rs
index 52000ade..c5200fe8 100644
--- a/test-framework/sudo-compliance-tests/src/sudoers.rs
+++ b/test-framework/sudo-compliance-tests/src/sudoers.rs
@@ -2,9 +2,6 @@ use sudo_test::{Command, Env, TextFile, User};
 
 use crate::{Result, PASSWORD, SUDOERS_ROOT_ALL_NOPASSWD, USERNAME};
 
-mod cmnd;
-mod host_list;
-mod run_as;
 mod user_list;
 
 #[test]
@@ -99,15 +96,3 @@ fn user_specifications_evaluated_bottom_to_top() -> Result<()> {
         .exec(&env)?
         .assert_success()
 }
-
-#[test]
-fn accepts_sudoers_file_that_has_no_trailing_newline() -> Result<()> {
-    let env = Env(TextFile(SUDOERS_ROOT_ALL_NOPASSWD).no_trailing_newline())
-        .user(User(USERNAME).password(PASSWORD))
-        .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .exec(&env)?
-        .assert_success()
-}
diff --git a/test-framework/sudo-compliance-tests/src/sudoers/cmnd.rs b/test-framework/sudo-compliance-tests/src/sudoers/cmnd.rs
deleted file mode 100644
index a0c2eff7..00000000
--- a/test-framework/sudo-compliance-tests/src/sudoers/cmnd.rs
+++ /dev/null
@@ -1,190 +0,0 @@
-//! Test the Cmnd_Spec component of the user specification: <user> ALL=(ALL:ALL) <cmnd_spec>
-
-use pretty_assertions::assert_eq;
-use sudo_test::{Command, Env};
-
-use crate::{Result, USERNAME};
-
-#[test]
-fn given_specific_command_then_that_command_is_allowed() -> Result<()> {
-    let env = Env("ALL ALL=(ALL:ALL) /bin/true").build()?;
-
-    Command::new("sudo")
-        .arg("/bin/true")
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn given_specific_command_then_other_command_is_not_allowed() -> Result<()> {
-    let env = Env("ALL ALL=(ALL:ALL) /bin/ls").build()?;
-
-    let output = Command::new("sudo").arg("/bin/true").exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(
-            output.stderr(),
-            "user root is not allowed to execute '/bin/true' as root"
-        );
-    }
-
-    Ok(())
-}
-
-#[test]
-fn given_specific_command_with_nopasswd_tag_then_no_password_auth_is_required() -> Result<()> {
-    let env = Env("ALL ALL=(ALL:ALL) NOPASSWD: /bin/true")
-        .user(USERNAME)
-        .build()?;
-
-    Command::new("sudo")
-        .arg("/bin/true")
-        .as_user(USERNAME)
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn command_specified_not_by_absolute_path_is_rejected() -> Result<()> {
-    let env = Env("ALL ALL=(ALL:ALL) true").build()?;
-
-    let output = Command::new("sudo").arg("/bin/true").exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(output.stderr(), "syntax error");
-    }
-
-    Ok(())
-}
-
-#[test]
-fn different() -> Result<()> {
-    let env = Env("ALL ALL=(ALL:ALL) /bin/true, /bin/ls").build()?;
-
-    Command::new("sudo")
-        .arg("/bin/true")
-        .exec(&env)?
-        .assert_success()?;
-
-    let output = Command::new("sudo")
-        .args(["/bin/ls", "/root"])
-        .exec(&env)?
-        .stdout()?;
-
-    assert_eq!("", output);
-
-    Ok(())
-}
-
-// it applies not only to the command is next to but to all commands that follow
-#[test]
-fn nopasswd_is_sticky() -> Result<()> {
-    let env = Env("ALL ALL=(ALL:ALL) NOPASSWD: /bin/ls, /bin/true")
-        .user(USERNAME)
-        .build()?;
-
-    Command::new("sudo")
-        .arg("/bin/true")
-        .as_user(USERNAME)
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn repeated() -> Result<()> {
-    let env = Env("ALL ALL=(ALL:ALL) /bin/true, /bin/true").build()?;
-
-    Command::new("sudo")
-        .arg("/bin/true")
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn nopasswd_override() -> Result<()> {
-    let env = Env("ALL ALL=(ALL:ALL) /bin/true, NOPASSWD: /bin/true")
-        .user(USERNAME)
-        .build()?;
-
-    Command::new("sudo")
-        .arg("/bin/true")
-        .as_user(USERNAME)
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-#[ignore]
-fn runas_override() -> Result<()> {
-    let env = Env(format!("ALL ALL = (root) /bin/ls, ({USERNAME}) /bin/true"))
-        .user(USERNAME)
-        .build()?;
-
-    let stdout = Command::new("sudo")
-        .args(["/bin/ls", "/root"])
-        .exec(&env)?
-        .stdout()?;
-
-    assert_eq!("", stdout);
-
-    let output = Command::new("sudo")
-        .args(["-u", USERNAME, "/bin/ls"])
-        .exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(
-            output.stderr(),
-            "user root is not allowed to execute '/bin/ls' as ferris"
-        );
-    }
-
-    Command::new("sudo")
-        .args(["-u", "ferris", "/bin/true"])
-        .exec(&env)?
-        .assert_success()?;
-
-    let output = Command::new("sudo").args(["/bin/true"]).exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(
-            output.stderr(),
-            "user root is not allowed to execute '/bin/true' as root"
-        );
-    }
-
-    Ok(())
-}
-
-#[test]
-#[ignore]
-fn runas_override_repeated_cmnd_means_runas_union() -> Result<()> {
-    let env = Env(format!(
-        "ALL ALL = (root) /bin/true, ({USERNAME}) /bin/true"
-    ))
-    .user(USERNAME)
-    .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .exec(&env)?
-        .assert_success()?;
-
-    Command::new("sudo")
-        .args(["-u", USERNAME, "true"])
-        .exec(&env)?
-        .assert_success()?;
-
-    Ok(())
-}
diff --git a/test-framework/sudo-compliance-tests/src/sudoers/host_list.rs b/test-framework/sudo-compliance-tests/src/sudoers/host_list.rs
deleted file mode 100644
index 609d69d5..00000000
--- a/test-framework/sudo-compliance-tests/src/sudoers/host_list.rs
+++ /dev/null
@@ -1,94 +0,0 @@
-use sudo_test::{Command, Env};
-
-use crate::Result;
-
-#[test]
-fn given_specific_hostname_then_sudo_from_said_hostname_is_allowed() -> Result<()> {
-    let hostname = "container";
-    let env = Env(format!("ALL {hostname} = (ALL:ALL) ALL"))
-        .hostname(hostname)
-        .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn given_specific_hostname_then_sudo_from_different_hostname_is_rejected() -> Result<()> {
-    let env = Env("ALL remotehost = (ALL:ALL) ALL")
-        .hostname("container")
-        .build()?;
-
-    let output = Command::new("sudo").arg("true").exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(
-            output.stderr(),
-            "root is not allowed to run sudo on container"
-        );
-    }
-
-    Ok(())
-}
-
-#[test]
-fn different() -> Result<()> {
-    let env = Env("ALL remotehost, container = (ALL:ALL) ALL")
-        .hostname("container")
-        .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn repeated() -> Result<()> {
-    let env = Env("ALL container, container = (ALL:ALL) ALL")
-        .hostname("container")
-        .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn negation_rejects() -> Result<()> {
-    let env = Env("ALL remotehost, !container = (ALL:ALL) ALL")
-        .hostname("container")
-        .build()?;
-
-    let output = Command::new("sudo").arg("true").exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(
-            output.stderr(),
-            "root is not allowed to run sudo on container"
-        );
-    }
-
-    Ok(())
-}
-
-#[test]
-fn double_negative_is_positive() -> Result<()> {
-    let env = Env("ALL !!container = (ALL:ALL) ALL")
-        .hostname("container")
-        .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .exec(&env)?
-        .assert_success()
-}
diff --git a/test-framework/sudo-compliance-tests/src/sudoers/run_as.rs b/test-framework/sudo-compliance-tests/src/sudoers/run_as.rs
deleted file mode 100644
index e8284107..00000000
--- a/test-framework/sudo-compliance-tests/src/sudoers/run_as.rs
+++ /dev/null
@@ -1,282 +0,0 @@
-//! Test the run_as component of the user specification: <user> ALL=(<run_as>) ALL`
-
-use sudo_test::{Command, Env, User};
-
-use crate::{Result, GROUPNAME, PAMD_SUDO_PAM_PERMIT, USERNAME};
-
-// "If both Runas_Lists are empty, the command may only be run as the invoking user."
-#[test]
-#[ignore]
-fn when_empty_then_implicit_as_self_is_allowed() -> Result<()> {
-    let env = Env("ALL ALL=() NOPASSWD: ALL").user(USERNAME).build()?;
-
-    for user in ["root", USERNAME] {
-        Command::new("sudo")
-            .args(["true"])
-            .as_user(user)
-            .exec(&env)?
-            .assert_success()?;
-    }
-
-    Ok(())
-}
-
-#[test]
-fn when_empty_then_explicit_as_self_is_allowed() -> Result<()> {
-    let env = Env("ALL ALL=() NOPASSWD: ALL").user(USERNAME).build()?;
-
-    for user in ["root", USERNAME] {
-        Command::new("sudo")
-            .args(["-u", user, "true"])
-            .as_user(user)
-            .exec(&env)?
-            .assert_success()?;
-    }
-
-    Ok(())
-}
-
-#[test]
-fn when_empty_then_as_someone_else_is_not_allowed() -> Result<()> {
-    let env = Env("ALL ALL=() NOPASSWD: ALL").user(USERNAME).build()?;
-
-    let output = Command::new("sudo")
-        .args(["-u", USERNAME, "true"])
-        .exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(
-            output.stderr(),
-            "user root is not allowed to execute '/bin/true' as ferris"
-        );
-    }
-
-    Ok(())
-}
-
-#[test]
-#[ignore]
-fn when_empty_then_as_own_group_is_allowed() -> Result<()> {
-    let env = Env("ALL ALL=() NOPASSWD: ALL")
-        .group(USERNAME)
-        .user(User(USERNAME).secondary_group(USERNAME))
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        Command::new("sudo")
-            .args(["-g", user, "true"])
-            .as_user(user)
-            .exec(&env)?
-            .assert_success()?;
-    }
-
-    Ok(())
-}
-
-#[test]
-fn when_specific_user_then_as_that_user_is_allowed() -> Result<()> {
-    let env = Env(format!("ALL ALL=({USERNAME}) NOPASSWD: ALL"))
-        .user(USERNAME)
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        Command::new("sudo")
-            .args(["-u", USERNAME, "true"])
-            .as_user(user)
-            .exec(&env)?
-            .assert_success()?;
-    }
-
-    Ok(())
-}
-
-#[test]
-fn when_specific_user_then_as_a_different_user_is_not_allowed() -> Result<()> {
-    let env = Env("ALL ALL=(ferris) NOPASSWD: ALL")
-        .user("ferris")
-        .user("ghost")
-        .build()?;
-
-    let output = Command::new("sudo")
-        .args(["-u", "ghost", "true"])
-        .exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(
-            output.stderr(),
-            "user root is not allowed to execute '/bin/true' as ghost"
-        );
-    }
-
-    Ok(())
-}
-
-#[test]
-fn when_specific_user_then_as_self_is_not_allowed() -> Result<()> {
-    let env = Env(format!("ALL ALL=({USERNAME}) NOPASSWD: ALL")).build()?;
-
-    let output = Command::new("sudo").args(["true"]).exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(
-            output.stderr(),
-            "user root is not allowed to execute '/bin/true' as root"
-        );
-    }
-
-    Ok(())
-}
-
-// "If only the first is specified, the command may be run as any user in the list but no -g option
-// may be specified."
-#[test]
-fn when_only_user_is_specified_then_group_flag_is_not_allowed() -> Result<()> {
-    let env = Env(format!("ALL ALL=({USERNAME}) ALL"))
-        // NOPASSWD does not seem to apply to the regular user so use PAM to avoid password input
-        .file("/etc/pam.d/sudo", PAMD_SUDO_PAM_PERMIT)
-        .user(USERNAME)
-        .group(GROUPNAME)
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        let output = Command::new("sudo")
-            .args(["-g", GROUPNAME, "true"])
-            .as_user(user)
-            .exec(&env)?;
-
-        assert!(!output.status().success());
-        assert_eq!(Some(1), output.status().code());
-
-        if sudo_test::is_original_sudo() {
-            assert_contains!(
-                output.stderr(),
-                " is not allowed to execute '/bin/true' as "
-            );
-        }
-    }
-
-    Ok(())
-}
-
-#[test]
-#[ignore]
-fn when_specific_group_then_as_that_group_is_allowed() -> Result<()> {
-    let env = Env(format!("ALL ALL=(:{GROUPNAME}) NOPASSWD: ALL"))
-        .user(USERNAME)
-        .group(GROUPNAME)
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        Command::new("sudo")
-            .args(["-g", GROUPNAME, "true"])
-            .as_user(user)
-            .exec(&env)?
-            .assert_success()?;
-    }
-
-    Ok(())
-}
-
-#[test]
-fn when_specific_group_then_as_a_different_group_is_not_allowed() -> Result<()> {
-    let env = Env(format!("ALL ALL=(:{GROUPNAME})  ALL"))
-        // NOPASSWD does not seem to apply to the regular user so use PAM to avoid password input
-        .file("/etc/pam.d/sudo", PAMD_SUDO_PAM_PERMIT)
-        .user(USERNAME)
-        .group(GROUPNAME)
-        .group("ghosts")
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        let output = Command::new("sudo")
-            .args(["-g", "ghosts", "true"])
-            .as_user(user)
-            .exec(&env)?;
-
-        assert!(!output.status().success());
-        assert_eq!(Some(1), output.status().code());
-
-        if sudo_test::is_original_sudo() {
-            assert_contains!(output.stderr(), "is not allowed to execute '/bin/true' as ");
-        }
-    }
-
-    Ok(())
-}
-
-#[test]
-fn when_only_group_is_specified_then_as_some_user_is_not_allowed() -> Result<()> {
-    let env = Env(format!("ALL ALL=(:{GROUPNAME}) ALL"))
-        // NOPASSWD does not seem to apply to the regular user so use PAM to avoid password input
-        .file("/etc/pam.d/sudo", PAMD_SUDO_PAM_PERMIT)
-        .user(USERNAME)
-        .user("ghost")
-        .group(GROUPNAME)
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        let output = Command::new("sudo")
-            .args(["-u", "ghost", "true"])
-            .as_user(user)
-            .exec(&env)?;
-
-        assert!(!output.status().success());
-        assert_eq!(Some(1), output.status().code());
-
-        if sudo_test::is_original_sudo() {
-            assert_contains!(
-                output.stderr(),
-                " is not allowed to execute '/bin/true' as "
-            );
-        }
-    }
-
-    Ok(())
-}
-
-// "If both Runas_Lists are specified, the command may be run with any combination of users and
-// groups listed in their respective Runas_Lists."
-#[test]
-fn when_both_user_and_group_are_specified_then_as_that_user_is_allowed() -> Result<()> {
-    let env = Env(format!("ALL ALL=({USERNAME}:{GROUPNAME}) NOPASSWD: ALL"))
-        .user(USERNAME)
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        Command::new("sudo")
-            .args(["-u", USERNAME, "true"])
-            .as_user(user)
-            .exec(&env)?
-            .assert_success()?;
-    }
-
-    Ok(())
-}
-
-#[test]
-#[ignore]
-fn when_both_user_and_group_are_specified_then_as_that_group_is_allowed() -> Result<()> {
-    let env = Env(format!("ALL ALL=({USERNAME}:{GROUPNAME}) NOPASSWD: ALL"))
-        .user(USERNAME)
-        .group(GROUPNAME)
-        .build()?;
-
-    for user in ["root", USERNAME] {
-        Command::new("sudo")
-            .args(["-g", GROUPNAME, "true"])
-            .as_user(user)
-            .exec(&env)?
-            .assert_success()?;
-    }
-
-    Ok(())
-}
diff --git a/test-framework/sudo-compliance-tests/src/sudoers/user_list.rs b/test-framework/sudo-compliance-tests/src/sudoers/user_list.rs
index 5a0a83c6..759be8d5 100644
--- a/test-framework/sudo-compliance-tests/src/sudoers/user_list.rs
+++ b/test-framework/sudo-compliance-tests/src/sudoers/user_list.rs
@@ -3,7 +3,7 @@
 use pretty_assertions::assert_eq;
 use sudo_test::{Command, Env};
 
-use crate::{Result, PAMD_SUDO_PAM_PERMIT, USERNAME};
+use crate::{Result, USERNAME};
 
 #[test]
 fn no_match() -> Result<()> {
@@ -76,96 +76,3 @@ fn group_id() -> Result<()> {
         .exec(&env)?
         .assert_success()
 }
-
-#[test]
-fn many_different() -> Result<()> {
-    let env = Env(format!("root, {USERNAME} ALL=(ALL:ALL) NOPASSWD: ALL"))
-        .user(USERNAME)
-        .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .exec(&env)?
-        .assert_success()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .as_user(USERNAME)
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn many_repeated() -> Result<()> {
-    let env = Env("root, root ALL=(ALL:ALL) NOPASSWD: ALL").build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn double_negative_is_positive() -> Result<()> {
-    let env = Env("!!root ALL=(ALL:ALL) NOPASSWD: ALL")
-        .user(USERNAME)
-        .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .exec(&env)?
-        .assert_success()
-}
-
-#[test]
-fn negation_excludes_group_members() -> Result<()> {
-    let env = Env("%users, !ghost ALL=(ALL:ALL) ALL")
-        // use PAM to avoid `ghost` getting a password prompt
-        .file("/etc/pam.d/sudo", PAMD_SUDO_PAM_PERMIT)
-        // the primary group of all new users is `users`
-        .user("ferris")
-        .user("ghost")
-        .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .as_user("ferris")
-        .exec(&env)?
-        .assert_success()?;
-
-    let output = Command::new("sudo")
-        .arg("true")
-        .as_user("ghost")
-        .exec(&env)?;
-
-    assert!(!output.status().success());
-    assert_eq!(Some(1), output.status().code());
-
-    if sudo_test::is_original_sudo() {
-        assert_contains!(output.stderr(), "ghost is not in the sudoers file");
-    }
-
-    Ok(())
-}
-
-#[test]
-fn negation_is_order_sensitive() -> Result<()> {
-    // negated items at the start of a specifier list  are meaningless
-    let env = Env("!ghost, %users ALL=(ALL:ALL) NOPASSWD: ALL")
-        // the primary group of all new users is `users`
-        .user("ferris")
-        .user("ghost")
-        .build()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .as_user("ferris")
-        .exec(&env)?
-        .assert_success()?;
-
-    Command::new("sudo")
-        .arg("true")
-        .as_user("ghost")
-        .exec(&env)?
-        .assert_success()
-}
diff --git a/test-framework/sudo-test/src/docker.rs b/test-framework/sudo-test/src/docker.rs
index cb417d9e..c0cccbf4 100644
--- a/test-framework/sudo-test/src/docker.rs
+++ b/test-framework/sudo-test/src/docker.rs
@@ -8,7 +8,7 @@ use std::{
 
 use tempfile::NamedTempFile;
 
-use crate::{base_image, Result, SudoUnderTest};
+use crate::{Result, SudoUnderTest, BASE_IMAGE};
 
 pub use self::command::{Child, Command, Output};
 
@@ -21,18 +21,11 @@ pub struct Container {
 }
 
 impl Container {
-    #[cfg(test)]
-    fn new(image: &str) -> Result<Self> {
-        Self::new_with_hostname(image, None)
-    }
-
-    pub fn new_with_hostname(image: &str, hostname: Option<&str>) -> Result<Self> {
+    pub fn new(image: &str) -> Result<Self> {
         let mut docker_run = StdCommand::new("docker");
-        docker_run.args(["run", "--detach"]);
-        if let Some(hostname) = hostname {
-            docker_run.args(["--hostname", hostname]);
-        }
-        docker_run.args(["--rm", image]).args(DOCKER_RUN_COMMAND);
+        docker_run
+            .args(["run", "-d", "--rm", image])
+            .args(DOCKER_RUN_COMMAND);
         let id = run(&mut docker_run, None)?.stdout()?;
         validate_docker_id(&id, &docker_run)?;
 
@@ -106,7 +99,7 @@ pub fn build_base_image() -> Result<()> {
     let repo_root = repo_root();
     let mut cmd = StdCommand::new("docker");
 
-    cmd.args(["build", "-t", base_image()]);
+    cmd.args(["build", "-t", BASE_IMAGE]);
 
     match SudoUnderTest::from_env()? {
         SudoUnderTest::Ours => {
diff --git a/test-framework/sudo-test/src/lib.rs b/test-framework/sudo-test/src/lib.rs
index 86bf98f4..4787f96f 100644
--- a/test-framework/sudo-test/src/lib.rs
+++ b/test-framework/sudo-test/src/lib.rs
@@ -19,13 +19,7 @@ type Result<T> = core::result::Result<T, Error>;
 
 mod docker;
 
-fn base_image() -> &'static str {
-    if is_original_sudo() {
-        "sudo-test-og"
-    } else {
-        "sudo-test-rs"
-    }
-}
+const BASE_IMAGE: &str = env!("CARGO_CRATE_NAME");
 
 /// are we testing the original sudo?
 pub fn is_original_sudo() -> bool {
@@ -67,14 +61,15 @@ pub struct Env {
 #[allow(non_snake_case)]
 pub fn Env(sudoers: impl Into<TextFile>) -> EnvBuilder {
     let mut builder = EnvBuilder::default();
+    let mut sudoers = sudoers.into();
+    // HACK append newline to work around memorysafety/sudo-rs#102
+    sudoers.contents.push('\n');
     builder.file("/etc/sudoers", sudoers);
     builder
 }
 
 impl Command {
     /// executes the command in the specified test environment
-    ///
-    /// NOTE that the trailing newline from `stdout` and `stderr` will be removed
     pub fn exec(&self, env: &Env) -> Result<Output> {
         if let Some(username) = self.get_user() {
             assert!(
@@ -104,7 +99,6 @@ impl Command {
 pub struct EnvBuilder {
     files: HashMap<AbsolutePath, TextFile>,
     groups: HashMap<Groupname, Group>,
-    hostname: Option<String>,
     users: HashMap<Username, User>,
 }
 
@@ -164,12 +158,6 @@ impl EnvBuilder {
         self
     }
 
-    /// Sets the hostname of the container to the specified string
-    pub fn hostname(&mut self, hostname: impl AsRef<str>) -> &mut Self {
-        self.hostname = Some(hostname.as_ref().to_string());
-        self
-    }
-
     /// builds the test environment
     ///
     /// # Panics
@@ -184,7 +172,7 @@ impl EnvBuilder {
             docker::build_base_image().expect("fatal error: could not build the base Docker image")
         });
 
-        let container = Container::new_with_hostname(base_image(), self.hostname.as_deref())?;
+        let container = Container::new(BASE_IMAGE)?;
 
         let (mut usernames, user_ids) = getent_passwd(&container)?;
 
@@ -262,23 +250,15 @@ pub struct User {
     password: Option<String>,
 }
 
-/// creates a new user with the specified `name` and the following defaults:
-///
-/// - on Debian containers, primary group = `users` (GID=100)
-/// - automatically assigned user ID
-/// - no assigned secondary groups
-/// - no assigned password
-/// - home directory set to `/home/<name>` but not automatically created
+/// creates a new user with the specified `name`
 #[allow(non_snake_case)]
 pub fn User(name: impl AsRef<str>) -> User {
     name.as_ref().into()
 }
 
 impl User {
-    /// assigns this user to the specified *secondary* `group`
-    ///
-    /// NOTE on Debian containers, all new users will be assigned to the `users` primary group (GID=100)
-    pub fn secondary_group(mut self, group: impl AsRef<str>) -> Self {
+    /// assigns this user to the specified `group`
+    pub fn group(mut self, group: impl AsRef<str>) -> Self {
         let groupname = group.as_ref();
         assert!(
             !self.groups.contains(groupname),
@@ -291,12 +271,10 @@ impl User {
         self
     }
 
-    /// assigns this user to all the specified *secondary* `groups`
-    ///
-    /// NOTE on Debian containers, all new users will be assigned to the `users` primary group (GID=100)
-    pub fn secondary_groups(mut self, groups: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
+    /// assigns this user to all the specified `groups`
+    pub fn groups(mut self, groups: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
         for group in groups {
-            self = self.secondary_group(group);
+            self = self.group(group);
         }
         self
     }
@@ -409,16 +387,12 @@ impl From<&'_ str> for Group {
 /// a text file
 pub struct TextFile {
     contents: String,
-    trailing_newline: bool,
 
     chmod: String,
     chown: String,
 }
 
 /// creates a text file with the specified `contents`
-///
-/// NOTE by default, a trailing newline will be appended to the contents if it doesn't contain one.
-/// to omit the trailing newline use the `TextFile::no_trailing_newline` method
 #[allow(non_snake_case)]
 pub fn TextFile(contents: impl AsRef<str>) -> TextFile {
     contents.as_ref().into()
@@ -444,24 +418,8 @@ impl TextFile {
         self
     }
 
-    /// strips newlines from the end of the file
-    pub fn no_trailing_newline(mut self) -> Self {
-        self.trailing_newline = false;
-        self
-    }
-
     fn create(&self, path: &str, container: &Container) -> Result<()> {
-        let mut contents = self.contents.clone();
-
-        if self.trailing_newline {
-            if !contents.ends_with('\n') {
-                contents.push('\n');
-            }
-        } else if contents.ends_with('\n') {
-            contents.pop();
-        }
-
-        container.cp(path, &contents)?;
+        container.cp(path, &self.contents)?;
 
         container
             .exec(Command::new("chown").args([&self.chown, path]))?
@@ -478,7 +436,6 @@ impl From<String> for TextFile {
             contents,
             chmod: Self::DEFAULT_CHMOD.to_string(),
             chown: Self::DEFAULT_CHOWN.to_string(),
-            trailing_newline: true,
         }
     }
 }
@@ -619,7 +576,7 @@ mod tests {
     fn creating_user_part_of_existing_group_works() -> Result<()> {
         let groupname = "users";
         let env = EnvBuilder::default()
-            .user(User(USERNAME).secondary_group(groupname))
+            .user(User(USERNAME).group(groupname))
             .build()?;
 
         let stdout = Command::new("groups")
@@ -731,65 +688,4 @@ mod tests {
 
         Ok(())
     }
-
-    #[test]
-    fn setting_hostname_works() -> Result<()> {
-        let expected = "container";
-
-        let env = EnvBuilder::default().hostname(expected).build()?;
-
-        let actual = Command::new("hostname").exec(&env)?.stdout()?;
-        assert_eq!(expected, actual);
-
-        Ok(())
-    }
-
-    #[test]
-    fn trailing_newline_by_default() -> Result<()> {
-        let path_a = "/root/a";
-        let path_b = "/root/b";
-        let env = EnvBuilder::default()
-            .file(path_a, "hello")
-            .file(path_b, "hello\n")
-            .build()?;
-
-        let a_last_char = Command::new("tail")
-            .args(["-c1", path_a])
-            .exec(&env)?
-            .stdout()?;
-        assert_eq!("", a_last_char);
-
-        let b_last_char = Command::new("tail")
-            .args(["-c1", path_b])
-            .exec(&env)?
-            .stdout()?;
-        assert_eq!("", b_last_char);
-
-        Ok(())
-    }
-
-    #[test]
-    fn no_trailing_newline() -> Result<()> {
-        let path_a = "/root/a";
-        let path_b = "/root/b";
-        let env = EnvBuilder::default()
-            .file(path_a, TextFile("hello").no_trailing_newline())
-            .file(path_b, TextFile("hello\n").no_trailing_newline())
-            .build()?;
-
-        let a_last_char = Command::new("tail")
-            .args(["-c1", path_a])
-            .exec(&env)?
-            .stdout()?;
-        assert_eq!("o", a_last_char);
-
-        let b_last_char = Command::new("tail")
-            .args(["-c1", path_b])
-            .exec(&env)?
-            .stdout()?;
-
-        assert_eq!("o", b_last_char);
-
-        Ok(())
-    }
 }