From 0afdf018f980563ddd59e58ebc93a5daf247b9a6 Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Sat, 14 Jun 2025 20:12:51 -0400 Subject: [PATCH 01/10] again attempting to fix this hell of a git history --- .gitignore | 1 + sp-modules/actual/config-paths-needed.json | 59 +++++ sp-modules/actual/flake.nix | 41 ++++ sp-modules/actual/icon-lg.svg | 48 +++++ sp-modules/actual/icon.svg | 44 ++++ sp-modules/actual/module.nix | 239 +++++++++++++++++++++ 6 files changed, 432 insertions(+) create mode 100644 sp-modules/actual/config-paths-needed.json create mode 100644 sp-modules/actual/flake.nix create mode 100644 sp-modules/actual/icon-lg.svg create mode 100644 sp-modules/actual/icon.svg create mode 100644 sp-modules/actual/module.nix diff --git a/.gitignore b/.gitignore index 6e35230..d584aee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store userdata/userdata.json userdata/tokens.json hardware-configuration.nix diff --git a/sp-modules/actual/config-paths-needed.json b/sp-modules/actual/config-paths-needed.json new file mode 100644 index 0000000..2e4e2fc --- /dev/null +++ b/sp-modules/actual/config-paths-needed.json @@ -0,0 +1,59 @@ +[ + [ + "services", + "actual" + ], + [ + "systemd", + "services", + "actual" + ], + [ + "selfprivacy", + "domain" + ], + [ + "selfprivacy", + "useBinds" + ], + [ + "selfprivacy", + "modules", + "actual" + ], + [ + "selfprivacy", + "modules", + "auth", + "enable" + ], + [ + "selfprivacy", + "sso", + "enable" + ], + [ + "selfprivacy", + "passthru", + "auth", + "mkOAuth2ClientSecretFP" + ], + [ + "selfprivacy", + "passthru", + "auth", + "mkServiceAccountTokenFP" + ], + [ + "selfprivacy", + "passthru", + "auth", + "oauth2-discovery-url" + ], + [ + "selfprivacy", + "passthru", + "auth", + "oauth2-provider-name" + ] +] \ No newline at end of file diff --git a/sp-modules/actual/flake.nix b/sp-modules/actual/flake.nix new file mode 100644 index 0000000..beaefac --- /dev/null +++ b/sp-modules/actual/flake.nix @@ -0,0 +1,41 @@ +{ + description = "Flake description"; + + outputs = + { self }: + { + nixosModules.default = import ./module.nix; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "actual"; + name = "Actual"; + description = "Actual (aka Actual Budget) is a super fast and privacy-focused app for managing your finances."; + svgIcon = builtins.readFile ./icon.svg; + showUrl = true; + primarySubdomain = "subdomain"; + isMovable = false; + isRequired = false; + canBeBackedUp = true; + backupDescription = "Your budgets, settings, and account secrets (where applicable)."; + systemdServices = [ + "actual.service" + ]; + user = "actual"; + group = "actual"; + folders = [ + "/var/lib/actual" + ]; + + license = [ + lib.licenses.mit + ]; + homepage = "https://actualbudget.org/"; + sourcePage = "https://github.com/actualbudget/actual"; + # since this module hasn't been thoroughly tested, I'd advertise it as `experimental`, but is also a `community` class module + supportLevel = "experimental"; + }; + }; +} diff --git a/sp-modules/actual/icon-lg.svg b/sp-modules/actual/icon-lg.svg new file mode 100644 index 0000000..9f15e98 --- /dev/null +++ b/sp-modules/actual/icon-lg.svg @@ -0,0 +1,48 @@ + + + + diff --git a/sp-modules/actual/icon.svg b/sp-modules/actual/icon.svg new file mode 100644 index 0000000..762f5eb --- /dev/null +++ b/sp-modules/actual/icon.svg @@ -0,0 +1,44 @@ + + + + diff --git a/sp-modules/actual/module.nix b/sp-modules/actual/module.nix new file mode 100644 index 0000000..733391d --- /dev/null +++ b/sp-modules/actual/module.nix @@ -0,0 +1,239 @@ +{ + config, + lib, + pkgs, + ... +}: +let + # Just for convenience, this module's config values + sp = config.selfprivacy; + cfg = sp.modules.actual; + + is-auth-enabled = cfg.enableSso && config.selfprivacy.sso.enable; + oauthClientID = "actual"; + auth-passthru = config.selfprivacy.passthru.auth; + full-domain = "https://${cfg.subdomain}.${sp.domain}"; + redirect-uri = "${full-domain}/openid/callback"; + landing-uri = "${full-domain}/login"; + oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID; + adminsGroup = "sp.${oauthClientID}.admins"; + usersGroup = "sp.${oauthClientID}.users"; + + linuxUserOfService = "actual"; + linuxGroupOfService = "actual"; + + oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService; + oauthSecretDir = "/run/actual/shh"; + oauthSecretFile = "${oauthSecretDir}/totallynotasecretfile.env"; + # creates an env file with the oauth client secret and configures permissions for the actual user/group + oauthClientInjectScript = pkgs.writeShellScript "inject-oidc-secrets" '' + mkdir -p ${oauthSecretDir} + echo "ACTUAL_OPENID_CLIENT_SECRET=$(<${oauthClientSecretFP})" > ${oauthSecretFile} + chown actual:actual ${oauthSecretFile} + chmod 600 ${oauthSecretFile} + ''; +in +{ + # Here go the options you expose to the user. + options.selfprivacy.modules.actual = { + # This is required and must always be named "enable" + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable the Actual Budget server"; + }) + // { + meta = { + type = "enable"; + }; + }; + # This is required if your service stores data on disk + location = + (lib.mkOption { + type = lib.types.str; + description = "Data location"; + }) + // { + meta = { + type = "location"; + }; + }; + # This is required if your service needs a subdomain + subdomain = + (lib.mkOption { + default = "actual"; + type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + description = "Subdomain"; + }) + // { + meta = { + widget = "subdomain"; + type = "string"; + regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + weight = 0; + }; + }; + # Other options, that user sees directly. + # Refer to Module options reference to learn more. + enableSso = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Single Sign-On"; + }) + // { + meta = { + type = "bool"; + weight = 2; + }; + }; + enableDebug = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Debug Logging"; + }) + // { + meta = { + type = "bool"; + weight = 3; + }; + }; + }; + + # All your changes to the system must go to this config attrset. + # It MUST use lib.mkIf with an enable option. + # This makes sure your module only makes changes to the system + # if the module is enabled. + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + # prevent SSO from being enabled in the module config if SSO isn't available/is disabled + assertions = [ + { + assertion = cfg.enableSso -> sp.sso.enable; + message = "SSO cannot be enabled for Actual when SSO is disabled globally."; + } + ]; + # If your service stores data on disk, you have to mount a folder + # for this. useBinds is always true on modern SelfPrivacy installations + # but we keep this mkIf to keep migration flow possible. + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/actual" = { + device = "/volumes/${cfg.location}/actual"; + # Make sure that your service does not start before folder mounts + options = [ + "bind" + "x-systemd.required-by=actual.service" + "x-systemd.before=actual.service" + ]; + }; + }; + + # actual service config + services.actual = { + enable = true; + settings = { + port = 5006; + # default to only password logins + allowedLoginMethods = [ "password" ]; + }; + }; + # adding the user/group to be used by the service + users = { + users.actual = { + isSystemUser = true; + group = "actual"; + }; + groups.actual = {}; + }; + + systemd = { + services = { + actual = { + # extra guard against the service starting before the bind has been mounted + unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/actual"; + serviceConfig = { + Slice = "actual.slice"; + # override dynamic user since service from nixpkgs enables by default, but it doesn't work in the selfprivacy environment + DynamicUser = lib.mkForce false; + # use service user + User = "actual"; + # use service group + Group = "actual"; + }; + environment = + # tell actual to log debug info to the console if option is enabled + (lib.mkIf cfg.enableDebug { + DEBUG = "actual:config,actual-sensitive:config"; + } + ); + }; + }; + # Define the slice itself + slices.actual = { + description = "Actual server service slice"; + }; + }; + + # You can define a reverse proxy for your service like this + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + useACMEHost = sp.domain; + forceSSL = true; + locations = { + "/" = { + proxyPass = "http://127.0.0.1:5006"; + }; + }; + }; + } + # SSO config + (lib.mkIf is-auth-enabled { + services.actual = { + settings = { + # permit openid logins + allowedLoginMethods = [ "openid" ]; + # default to openid if enabled + loginMethod = "openid"; + # SSO config + openId = { + discoveryURL = oauthDiscoveryURL; + client_id = oauthClientID; + server_hostname = full-domain; + authMethod = "openid"; + }; + }; + }; + systemd.services.actual = { + serviceConfig={ + # run inject script with root privileges + ExecStartPre = "+${oauthClientInjectScript}"; + # use the file generated by the inject script, even if it doesn't yet exist + EnvironmentFile = "-${oauthSecretFile}"; + RuntimeDirectory = "actual"; + }; + }; + + # OIDC for Actual is currently in beta and requires legacy cryptography algorithms + services.kanidm.provision.systems.oauth2."${oauthClientID}".enableLegacyCrypto = true; + # Configure the OIDC client + selfprivacy.auth.clients."${oauthClientID}" = { + inherit adminsGroup usersGroup; + imageFile = ./icon-lg.svg; + displayName = "Actual"; + subdomain = cfg.subdomain; + originLanding = landing-uri; + originUrl = redirect-uri; + clientSystemdUnits = [ "actual.service" ]; + enablePkce = true; + linuxUserOfClient = linuxUserOfService; + linuxGroupOfClient = linuxGroupOfService; + useShortPreferredUsername = true; + scopeMaps.${usersGroup} = [ "email" "openid" "profile" ]; + }; + }) + ] + ); + +} From 579d736dd73484b6bf77f47ca95e27990577c165 Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:39:28 -0400 Subject: [PATCH 02/10] actual: test forcing oidc-only auth (no password) when sso is enabled --- sp-modules/actual/module.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sp-modules/actual/module.nix b/sp-modules/actual/module.nix index 733391d..53f5c60 100644 --- a/sp-modules/actual/module.nix +++ b/sp-modules/actual/module.nix @@ -192,8 +192,8 @@ in (lib.mkIf is-auth-enabled { services.actual = { settings = { - # permit openid logins - allowedLoginMethods = [ "openid" ]; + # only permit openid logins + allowedLoginMethods = lib.mkForce [ "openid" ]; # default to openid if enabled loginMethod = "openid"; # SSO config From 6366116c54b4327d8192f81c9869a2617afde5ad Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:06:15 -0400 Subject: [PATCH 03/10] update flake description --- sp-modules/actual/flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/actual/flake.nix b/sp-modules/actual/flake.nix index beaefac..33a79e0 100644 --- a/sp-modules/actual/flake.nix +++ b/sp-modules/actual/flake.nix @@ -1,5 +1,5 @@ { - description = "Flake description"; + description = "Actual (aka Actual Budget) is a super fast and privacy-focused app for managing your finances."; outputs = { self }: From 62e90db10f562800c400b5a609019a94bffb9fe2 Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:07:02 -0400 Subject: [PATCH 04/10] enable `isMovable` --- sp-modules/actual/flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/actual/flake.nix b/sp-modules/actual/flake.nix index 33a79e0..f71c462 100644 --- a/sp-modules/actual/flake.nix +++ b/sp-modules/actual/flake.nix @@ -16,7 +16,7 @@ svgIcon = builtins.readFile ./icon.svg; showUrl = true; primarySubdomain = "subdomain"; - isMovable = false; + isMovable = true; isRequired = false; canBeBackedUp = true; backupDescription = "Your budgets, settings, and account secrets (where applicable)."; From 9dc3af43e3c940d3365080fb99b5c5e41133d454 Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:08:06 -0400 Subject: [PATCH 05/10] change module to 'community' type --- sp-modules/actual/flake.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sp-modules/actual/flake.nix b/sp-modules/actual/flake.nix index f71c462..95031e8 100644 --- a/sp-modules/actual/flake.nix +++ b/sp-modules/actual/flake.nix @@ -34,8 +34,7 @@ ]; homepage = "https://actualbudget.org/"; sourcePage = "https://github.com/actualbudget/actual"; - # since this module hasn't been thoroughly tested, I'd advertise it as `experimental`, but is also a `community` class module - supportLevel = "experimental"; + supportLevel = "community"; }; }; } From 5039b3d2e93f67a79594cb9c8580cc2f7b4790ac Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:09:28 -0400 Subject: [PATCH 06/10] enable sso by default --- sp-modules/actual/module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp-modules/actual/module.nix b/sp-modules/actual/module.nix index 53f5c60..e535010 100644 --- a/sp-modules/actual/module.nix +++ b/sp-modules/actual/module.nix @@ -78,7 +78,7 @@ in # Refer to Module options reference to learn more. enableSso = (lib.mkOption { - default = false; + default = true; type = lib.types.bool; description = "Enable Single Sign-On"; }) From 7135f5b6bdec5829b51bd056822239a2733d3c14 Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:17:16 -0400 Subject: [PATCH 07/10] trimmed comments --- sp-modules/actual/module.nix | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/sp-modules/actual/module.nix b/sp-modules/actual/module.nix index e535010..53c4fe5 100644 --- a/sp-modules/actual/module.nix +++ b/sp-modules/actual/module.nix @@ -34,9 +34,7 @@ let ''; in { - # Here go the options you expose to the user. options.selfprivacy.modules.actual = { - # This is required and must always be named "enable" enable = (lib.mkOption { default = false; @@ -48,7 +46,6 @@ in type = "enable"; }; }; - # This is required if your service stores data on disk location = (lib.mkOption { type = lib.types.str; @@ -59,7 +56,6 @@ in type = "location"; }; }; - # This is required if your service needs a subdomain subdomain = (lib.mkOption { default = "actual"; @@ -74,8 +70,7 @@ in weight = 0; }; }; - # Other options, that user sees directly. - # Refer to Module options reference to learn more. + # service settings enableSso = (lib.mkOption { default = true; @@ -102,10 +97,6 @@ in }; }; - # All your changes to the system must go to this config attrset. - # It MUST use lib.mkIf with an enable option. - # This makes sure your module only makes changes to the system - # if the module is enabled. config = lib.mkIf cfg.enable ( lib.mkMerge [ { @@ -116,13 +107,10 @@ in message = "SSO cannot be enabled for Actual when SSO is disabled globally."; } ]; - # If your service stores data on disk, you have to mount a folder - # for this. useBinds is always true on modern SelfPrivacy installations - # but we keep this mkIf to keep migration flow possible. + fileSystems = lib.mkIf sp.useBinds { "/var/lib/actual" = { device = "/volumes/${cfg.location}/actual"; - # Make sure that your service does not start before folder mounts options = [ "bind" "x-systemd.required-by=actual.service" @@ -171,13 +159,11 @@ in ); }; }; - # Define the slice itself slices.actual = { description = "Actual server service slice"; }; }; - # You can define a reverse proxy for your service like this services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { useACMEHost = sp.domain; forceSSL = true; @@ -188,6 +174,7 @@ in }; }; } + # SSO config (lib.mkIf is-auth-enabled { services.actual = { @@ -196,7 +183,7 @@ in allowedLoginMethods = lib.mkForce [ "openid" ]; # default to openid if enabled loginMethod = "openid"; - # SSO config + # service SSO config openId = { discoveryURL = oauthDiscoveryURL; client_id = oauthClientID; @@ -217,7 +204,6 @@ in # OIDC for Actual is currently in beta and requires legacy cryptography algorithms services.kanidm.provision.systems.oauth2."${oauthClientID}".enableLegacyCrypto = true; - # Configure the OIDC client selfprivacy.auth.clients."${oauthClientID}" = { inherit adminsGroup usersGroup; imageFile = ./icon-lg.svg; From da096e05c8df98218a8f9b2d5d484fb274543938 Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:28:56 -0400 Subject: [PATCH 08/10] apparently we don't need `adminsGroup` --- sp-modules/actual/module.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sp-modules/actual/module.nix b/sp-modules/actual/module.nix index 53c4fe5..6a83fb4 100644 --- a/sp-modules/actual/module.nix +++ b/sp-modules/actual/module.nix @@ -16,7 +16,6 @@ let redirect-uri = "${full-domain}/openid/callback"; landing-uri = "${full-domain}/login"; oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID; - adminsGroup = "sp.${oauthClientID}.admins"; usersGroup = "sp.${oauthClientID}.users"; linuxUserOfService = "actual"; @@ -205,7 +204,7 @@ in # OIDC for Actual is currently in beta and requires legacy cryptography algorithms services.kanidm.provision.systems.oauth2."${oauthClientID}".enableLegacyCrypto = true; selfprivacy.auth.clients."${oauthClientID}" = { - inherit adminsGroup usersGroup; + inherit usersGroup; imageFile = ./icon-lg.svg; displayName = "Actual"; subdomain = cfg.subdomain; From 425e3eeec28323b65060b940d9d46b6d5dd41046 Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:30:16 -0400 Subject: [PATCH 09/10] fix for oidc user registration flow in 25.5.0 --- sp-modules/actual/module.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sp-modules/actual/module.nix b/sp-modules/actual/module.nix index 6a83fb4..13b6394 100644 --- a/sp-modules/actual/module.nix +++ b/sp-modules/actual/module.nix @@ -182,6 +182,8 @@ in allowedLoginMethods = lib.mkForce [ "openid" ]; # default to openid if enabled loginMethod = "openid"; + # https://github.com/actualbudget/actual/pull/4421 + userCreationMode = "login"; # service SSO config openId = { discoveryURL = oauthDiscoveryURL; From b0c3d835cb54076a01943d8afdff0e9c728084ec Mon Sep 17 00:00:00 2001 From: cl0vrfi3ld <47996003+cl0vrfi3ld@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:36:48 -0400 Subject: [PATCH 10/10] users n stuff --- sp-modules/actual/module.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sp-modules/actual/module.nix b/sp-modules/actual/module.nix index 13b6394..d7297fc 100644 --- a/sp-modules/actual/module.nix +++ b/sp-modules/actual/module.nix @@ -146,9 +146,9 @@ in # override dynamic user since service from nixpkgs enables by default, but it doesn't work in the selfprivacy environment DynamicUser = lib.mkForce false; # use service user - User = "actual"; + User = linuxUserOfService; # use service group - Group = "actual"; + Group = linuxGroupOfService; }; environment = # tell actual to log debug info to the console if option is enabled