226 lines
6.7 KiB
Nix
226 lines
6.7 KiB
Nix
{
|
|
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
|
|
{
|
|
options.selfprivacy.modules.actual = {
|
|
enable =
|
|
(lib.mkOption {
|
|
default = false;
|
|
type = lib.types.bool;
|
|
description = "Enable the Actual Budget server";
|
|
})
|
|
// {
|
|
meta = {
|
|
type = "enable";
|
|
};
|
|
};
|
|
location =
|
|
(lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Data location";
|
|
})
|
|
// {
|
|
meta = {
|
|
type = "location";
|
|
};
|
|
};
|
|
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;
|
|
};
|
|
};
|
|
# service settings
|
|
enableSso =
|
|
(lib.mkOption {
|
|
default = true;
|
|
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;
|
|
};
|
|
};
|
|
};
|
|
|
|
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.";
|
|
}
|
|
];
|
|
|
|
fileSystems = lib.mkIf sp.useBinds {
|
|
"/var/lib/actual" = {
|
|
device = "/volumes/${cfg.location}/actual";
|
|
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";
|
|
}
|
|
);
|
|
};
|
|
};
|
|
slices.actual = {
|
|
description = "Actual server service slice";
|
|
};
|
|
};
|
|
|
|
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 = {
|
|
# only permit openid logins
|
|
allowedLoginMethods = lib.mkForce [ "openid" ];
|
|
# default to openid if enabled
|
|
loginMethod = "openid";
|
|
# service 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;
|
|
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" ];
|
|
};
|
|
})
|
|
]
|
|
);
|
|
|
|
}
|