diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
index 3f53d67f72c..a0f001d0f92 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
@@ -216,6 +216,13 @@
services.hadoop.hbase.
+
+
+ Please,
+ a Sudo clone written in Rust. Available as
+ security.please
+
+
Sachet,
diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md
index 505c10376b5..76b998d9e87 100644
--- a/nixos/doc/manual/release-notes/rl-2211.section.md
+++ b/nixos/doc/manual/release-notes/rl-2211.section.md
@@ -79,6 +79,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [HBase cluster](https://hbase.apache.org/), a distributed, scalable, big data store. Available as [services.hadoop.hbase](options.html#opt-services.hadoop.hbase.enable).
+- [Please](https://github.com/edneville/please), a Sudo clone written in Rust. Available as [security.please](#opt-security.please.enable)
+
- [Sachet](https://github.com/messagebird/sachet/), an SMS alerting tool for the Prometheus Alertmanager. Available as [services.prometheus.sachet](#opt-services.prometheus.sachet.enable).
- [infnoise](https://github.com/leetronics/infnoise), a hardware True Random Number Generator dongle.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 9fc3af4b1ce..c400fadef3a 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -263,6 +263,7 @@
./security/pam.nix
./security/pam_usb.nix
./security/pam_mount.nix
+ ./security/please.nix
./security/polkit.nix
./security/rngd.nix
./security/rtkit.nix
diff --git a/nixos/modules/security/please.nix b/nixos/modules/security/please.nix
new file mode 100644
index 00000000000..88bb9cba2bf
--- /dev/null
+++ b/nixos/modules/security/please.nix
@@ -0,0 +1,122 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.security.please;
+ ini = pkgs.formats.ini { };
+in
+{
+ options.security.please = {
+ enable = mkEnableOption (mdDoc ''
+ please, a Sudo clone which allows a users to execute a command or edit a
+ file as another user
+ '');
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.please;
+ defaultText = literalExpression "pkgs.please";
+ description = mdDoc ''
+ Which package to use for {command}`please`.
+ '';
+ };
+
+ wheelNeedsPassword = mkOption {
+ type = types.bool;
+ default = true;
+ description = lib.mdDoc ''
+ Whether users of the `wheel` group must provide a password to run
+ commands or edit files with {command}`please` and
+ {command}`pleaseedit` respectively.
+ '';
+ };
+
+ settings = mkOption {
+ type = ini.type;
+ default = { };
+ example = {
+ jim_run_any_as_root = {
+ name = "jim";
+ type = "run";
+ target = "root";
+ rule = ".*";
+ require_pass = false;
+ };
+ jim_edit_etc_hosts_as_root = {
+ name = "jim";
+ type = "edit";
+ target = "root";
+ rule = "/etc/hosts";
+ editmode = 644;
+ require_pass = true;
+ };
+ };
+ description = mdDoc ''
+ Please configuration. Refer to
+ for
+ details.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ security.wrappers =
+ let
+ owner = "root";
+ group = "root";
+ setuid = true;
+ in
+ {
+ please = {
+ source = "${cfg.package}/bin/please";
+ inherit owner group setuid;
+ };
+ pleaseedit = {
+ source = "${cfg.package}/bin/pleaseedit";
+ inherit owner group setuid;
+ };
+ };
+
+ security.please.settings = rec {
+ # The "wheel" group is allowed to do anything by default but this can be
+ # overridden.
+ wheel_run_as_any = {
+ type = "run";
+ group = true;
+ name = "wheel";
+ target = ".*";
+ rule = ".*";
+ require_pass = cfg.wheelNeedsPassword;
+ };
+ wheel_edit_as_any = wheel_run_as_any // { type = "edit"; };
+ wheel_list_as_any = wheel_run_as_any // { type = "list"; };
+ };
+
+ environment = {
+ systemPackages = [ cfg.package ];
+
+ etc."please.ini".source = ini.generate "please.ini"
+ (cfg.settings // (rec {
+ # The "root" user is allowed to do anything by default and this cannot
+ # be overridden.
+ root_run_as_any = {
+ type = "run";
+ name = "root";
+ target = ".*";
+ rule = ".*";
+ require_pass = false;
+ };
+ root_edit_as_any = root_run_as_any // { type = "edit"; };
+ root_list_as_any = root_run_as_any // { type = "list"; };
+ }));
+ };
+
+ security.pam.services.please = {
+ sshAgentAuth = true;
+ usshAuth = true;
+ };
+
+ meta.maintainers = with maintainers; [ azahi ];
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 5026bbf36dd..0fc08e841ec 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -491,6 +491,7 @@ in {
plasma5 = handleTest ./plasma5.nix {};
plasma5-systemd-start = handleTest ./plasma5-systemd-start.nix {};
plausible = handleTest ./plausible.nix {};
+ please = handleTest ./please.nix {};
pleroma = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./pleroma.nix {};
plikd = handleTest ./plikd.nix {};
plotinus = handleTest ./plotinus.nix {};
diff --git a/nixos/tests/please.nix b/nixos/tests/please.nix
new file mode 100644
index 00000000000..2437cfe1613
--- /dev/null
+++ b/nixos/tests/please.nix
@@ -0,0 +1,66 @@
+import ./make-test-python.nix ({ lib, ... }:
+{
+ name = "please";
+ meta.maintainers = with lib.maintainers; [ azahi ];
+
+ nodes.machine =
+ { ... }:
+ {
+ users.users = with lib; mkMerge [
+ (listToAttrs (map
+ (n: nameValuePair n { isNormalUser = true; })
+ (genList (x: "user${toString x}") 6)))
+ {
+ user0.extraGroups = [ "wheel" ];
+ }
+ ];
+
+ security.please = {
+ enable = true;
+ wheelNeedsPassword = false;
+ settings = {
+ user2_run_true_as_root = {
+ name = "user2";
+ target = "root";
+ rule = "/run/current-system/sw/bin/true";
+ require_pass = false;
+ };
+ user4_edit_etc_hosts_as_root = {
+ name = "user4";
+ type = "edit";
+ target = "root";
+ rule = "/etc/hosts";
+ editmode = 644;
+ require_pass = false;
+ };
+ };
+ };
+ };
+
+ testScript = ''
+ with subtest("root: can run anything by default"):
+ machine.succeed('please true')
+ with subtest("root: can edit anything by default"):
+ machine.succeed('EDITOR=cat pleaseedit /etc/hosts')
+
+ with subtest("user0: can run as root because it's in the wheel group"):
+ machine.succeed('su - user0 -c "please -u root true"')
+ with subtest("user1: cannot run as root because it's not in the wheel group"):
+ machine.fail('su - user1 -c "please -u root true"')
+
+ with subtest("user0: can edit as root"):
+ machine.succeed('su - user0 -c "EDITOR=cat pleaseedit /etc/hosts"')
+ with subtest("user1: cannot edit as root"):
+ machine.fail('su - user1 -c "EDITOR=cat pleaseedit /etc/hosts"')
+
+ with subtest("user2: can run 'true' as root"):
+ machine.succeed('su - user2 -c "please -u root true"')
+ with subtest("user3: cannot run 'true' as root"):
+ machine.fail('su - user3 -c "please -u root true"')
+
+ with subtest("user4: can edit /etc/hosts"):
+ machine.succeed('su - user4 -c "EDITOR=cat pleaseedit /etc/hosts"')
+ with subtest("user5: cannot edit /etc/hosts"):
+ machine.fail('su - user5 -c "EDITOR=cat pleaseedit /etc/hosts"')
+ '';
+})
diff --git a/pkgs/tools/security/please/default.nix b/pkgs/tools/security/please/default.nix
index b3317dc2a82..40640ba4ed8 100644
--- a/pkgs/tools/security/please/default.nix
+++ b/pkgs/tools/security/please/default.nix
@@ -29,6 +29,8 @@ rustPlatform.buildRustPackage rec {
installManPage man/*
'';
+ passthru.tests = { inherit (nixosTests) please; };
+
meta = with lib; {
description = "A polite regex-first sudo alternative";
longDescription = ''