From 18ea6d7a8f8e6b5378db269ec6cdbb38f22d0356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20Schwarz=C3=A4ugl?= Date: Sat, 9 Aug 2025 16:23:53 +0200 Subject: [PATCH] pizauth: init module --- .../misc/news/2025/08/2025-08-09_21-07-00.nix | 12 ++ modules/services/pizauth.nix | 190 ++++++++++++++++++ .../modules/services/pizauth/basic-config.nix | 70 +++++++ tests/modules/services/pizauth/default.nix | 2 + 4 files changed, 274 insertions(+) create mode 100644 modules/misc/news/2025/08/2025-08-09_21-07-00.nix create mode 100644 modules/services/pizauth.nix create mode 100644 tests/modules/services/pizauth/basic-config.nix create mode 100644 tests/modules/services/pizauth/default.nix diff --git a/modules/misc/news/2025/08/2025-08-09_21-07-00.nix b/modules/misc/news/2025/08/2025-08-09_21-07-00.nix new file mode 100644 index 000000000..861ad21f3 --- /dev/null +++ b/modules/misc/news/2025/08/2025-08-09_21-07-00.nix @@ -0,0 +1,12 @@ +{ pkgs, ... }: +{ + time = "2025-08-09T22:11:00+00:00"; + condition = pkgs.stdenv.hostPlatform.isLinux; + message = '' + A new service is available: 'services.pizauth'. + + Pizauth is a simple program for requesting, showing, and refreshing OAuth2 access tokens. + Pizauth is formed of two components: a persistent server which interacts with the user to request tokens, and refreshes them as necessary; + and a command-line interface which can be used by programs such as fdm and msmtp to authenticate with OAuth2. + ''; +} diff --git a/modules/services/pizauth.nix b/modules/services/pizauth.nix new file mode 100644 index 000000000..0b77b0e1e --- /dev/null +++ b/modules/services/pizauth.nix @@ -0,0 +1,190 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib) mkOption types; + + cfg = config.services.pizauth; +in +{ + meta.maintainers = [ lib.hm.maintainers.swarsel ]; + + options.services.pizauth = { + enable = lib.mkEnableOption '' + Pizauth, a commandline OAuth2 authentication daemon + ''; + + package = lib.mkPackageOption pkgs "pizauth" { }; + + extraConfig = mkOption { + type = types.nullOr types.str; + default = null; + description = "Additional global configuration. See pizauth.conf(5) for a available options."; + }; + + accounts = mkOption { + type = types.attrsOf ( + types.submodule { + options = { + name = mkOption { + type = types.str; + readOnly = true; + description = '' + Unique identifier of the account. This is set to the + attribute name of the account configuration. + ''; + }; + + authUri = mkOption { + type = types.str; + description = '' + The OAuth2 server's authentication URI. + ''; + }; + + tokenUri = mkOption { + type = types.str; + description = '' + The OAuth2 server's token URI. + ''; + }; + + clientId = mkOption { + type = types.str; + description = '' + The OAuth2 client ID. + ''; + }; + + clientSecret = mkOption { + type = types.str; + description = '' + The OAuth2 client secret. + ''; + }; + + scopes = mkOption { + type = types.nullOr (types.listOf types.str); + description = '' + The scopes which the OAuth2 token will give access to. Optional. + Note that Office365 requires the non-standard "offline_access" scope to be specified in order for pizauth to be able to operate successfully. + ''; + default = [ ]; + example = [ + "https://outlook.office365.com/IMAP.AccessAsUser.All" + "https://outlook.office365.com/SMTP.Send" + "offline_access" + ]; + }; + + loginHint = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + An optional login hint for the account provider. + ''; + }; + + extraConfig = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Additional configuration that will be added to the account configuration. See pizauth.conf(5) for available options. + ''; + }; + }; + } + ); + default = { }; + description = "Pizauth accounts that should be configured"; + }; + + }; + + config = lib.mkIf cfg.enable { + assertions = [ (lib.hm.assertions.assertPlatform "services.pizauth" pkgs lib.platforms.linux) ]; + + home.packages = [ cfg.package ]; + + xdg.configFile."pizauth.conf".source = + let + indent = " "; + + renderScopes = + scopes: + let + quoted = map (s: "\"${s}\"") scopes; + joined = lib.concatStringsSep ",\n${indent}${indent}" quoted; + in + "[\n${indent}${indent}${joined}\n${indent}]"; + + renderAccount = + name: acc: + '' + account "${name}" { + ${indent}auth_uri = "${acc.authUri}"; + ${indent}token_uri = "${acc.tokenUri}"; + ${indent}client_id = "${acc.clientId}"; + ${indent}client_secret = "${acc.clientSecret}"; + '' + + lib.optionalString (acc.scopes != [ ] && acc.scopes != null) '' + ${indent}scopes = ${renderScopes acc.scopes}; + '' + + lib.optionalString (acc.loginHint != "" && acc.loginHint != null) '' + ${indent}login_hint = "${acc.loginHint}"; + '' + + lib.optionalString (acc.extraConfig != "" && acc.extraConfig != null) ( + let + indentedExtraConfig = lib.concatMapStringsSep "\n" ( + line: if line == "" then "" else "${indent}${line}" + ) (lib.splitString "\n" acc.extraConfig); + in + indentedExtraConfig + ) + + '' + } + ''; + + renderedAccounts = lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: acc: renderAccount name acc) cfg.accounts + ); + + in + pkgs.writeTextFile { + name = "pizauth.conf"; + text = + lib.optionalString (cfg.extraConfig != null && cfg.extraConfig != "") "${cfg.extraConfig}\n" + + renderedAccounts; + }; + + systemd.user.services.pizauth = { + Unit = { + Description = "Pizauth OAuth2 token manager"; + After = [ "network.target" ]; + }; + + Service = { + Type = "simple"; + ExecStart = "${lib.getExe cfg.package} server -vvvv -d"; + ExecReload = "${lib.getExe cfg.package} reload"; + ExecStop = "${lib.getExe cfg.package} shutdown"; + Restart = "on-failure"; + + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateUsers = true; + RestrictNamespaces = true; + SystemCallArchitectures = "native"; + SystemCallFilter = "@system-service"; + }; + + Install = { + WantedBy = [ "default.target" ]; + }; + }; + }; +} diff --git a/tests/modules/services/pizauth/basic-config.nix b/tests/modules/services/pizauth/basic-config.nix new file mode 100644 index 000000000..9578dd9fa --- /dev/null +++ b/tests/modules/services/pizauth/basic-config.nix @@ -0,0 +1,70 @@ +{ pkgs, ... }: +{ + config = { + services.pizauth = { + enable = true; + extraConfig = '' + refresh_at_least = 15s; + ''; + accounts = { + test1 = { + authUri = "authUri1"; + tokenUri = "tokenUri1"; + clientId = "clientId1"; + clientSecret = "clientSecret1"; + loginHint = "testLogin1"; + extraConfig = '' + redirectUri = "redirectUri1"; + refresh_retry = 30s; + ''; + }; + test2 = { + authUri = "authUri2"; + tokenUri = "tokenUri2"; + clientId = "clientId2"; + clientSecret = "clientSecret2"; + scopes = [ + "scope1" + "offline_access" + ]; + }; + }; + }; + + test.stubs.pizauth = { }; + + nmt.script = '' + local serviceFile=home-files/.config/systemd/user/pizauth.service + + assertFileExists $serviceFile + assertFileRegex $serviceFile 'ExecStart=.*/bin/dummy server -vvvv -d' + + assertFileExists home-files/.config/pizauth.conf + assertFileContent home-files/.config/pizauth.conf \ + ${pkgs.writeText "expected-config" '' + refresh_at_least = 15s; + + account "test1" { + auth_uri = "authUri1"; + token_uri = "tokenUri1"; + client_id = "clientId1"; + client_secret = "clientSecret1"; + login_hint = "testLogin1"; + redirectUri = "redirectUri1"; + refresh_retry = 30s; + } + + account "test2" { + auth_uri = "authUri2"; + token_uri = "tokenUri2"; + client_id = "clientId2"; + client_secret = "clientSecret2"; + scopes = [ + "scope1", + "offline_access" + ]; + } + ''} + ''; + }; +} diff --git a/tests/modules/services/pizauth/default.nix b/tests/modules/services/pizauth/default.nix new file mode 100644 index 000000000..99667867a --- /dev/null +++ b/tests/modules/services/pizauth/default.nix @@ -0,0 +1,2 @@ +{ lib, pkgs, ... }: +lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { pizauth-basic-config = ./basic-config.nix; }